Chapter 2. Traits and Mixin Compositions

Before digging into some actual design patterns, we have to make sure that many of the Scala language concepts are clear to the reader. Many of those concepts will later be used in implementing the actual design patterns, and being aware of the possibilities, limitations, and pitfalls are the key factors that enable to correctly and efficiently write code. Even though those concepts are not considered as "official" design patterns, they could still be used to write good software. Even in some cases, due to the richness of Scala, some concepts could replace a design pattern by just using language features. After all, as we have already said before, design patterns are there because a programming language lacks features and is not rich enough to complete certain tasks.

The first topic that we will look into is about traits and mixin compositions. They provide the developer with a possibility to share already implemented functionality or to define interfaces for classes in an application. Many of the possibilities, which are provided by traits and mixin compositions to developers, are useful to implement some of the design patterns that we will focus on later in this book. We will go through the following main topics in this chapter:

  • Traits
  • Mixin compositions
  • Multiple inheritance
  • Linearization
  • Testing traits
  • Traits versus classes

Traits

Many of you might have different perspectives of traits in Scala. They can be viewed not only as interfaces in other languages, but also as classes with only parameter-less constructors.

Tip

Trait parameters

The Scala programming language is quite dynamic and it has evolved quickly. One of the directions that will be investigated for the 2.12 version of the language are trait parameters. More information can be found at http://www.scala-lang.org/news/roadmap-next/.

In the following few sections, we will we will see the traits from different points of view and try to give you some ideas about how they can be used.

Traits as interfaces

Traits can be viewed as interfaces in other languages, for example, Java. They, however, allow the developers to implement some or all of their methods. Whenever there is some code in a trait, the trait is called a mixin. Let's have a look at the following example:

trait Alarm { 
  def trigger(): String 
}

Here Alarm is an interface. Its only method, trigger, does not have any implementation and if mixed in a non-abstract class, an implementation of the method will be required.

Let's see another trait example:

trait Notifier { 
  val notificationMessage: String 
  
  def printNotification(): Unit = { 
    System.out.println(notificationMessage) 
  } 
  
  def clear() 
}

The Notifier interface shown previously has one of its methods implemented, and clear and the value of notificationMessage have to be handled by the classes that will mix with the Notifier interface. Moreover, the traits can require a class to have a specific variable inside it. This is somewhat similar to abstract classes in other languages.

Mixing in traits with variables

As we just pointed out, traits might require a class to have a specific variable. An interesting use case would be when we pass a variable to the constructor of a class. This will cover the trait requirements:

class NotifierImpl(val notificationMessage: String) extends Notifier { 
  override def clear(): Unit = System.out.println("cleared") 
}

The only requirement here is for the variable to have the same name and to be preceded by the val keyword in the class definition. If we don't use val in front of the parameter in the preceding code, the compiler would still ask us to implement the trait. In this case, we would have to use a different name for the class parameter and would have an override val notificationMessage assignment in the class body. The reason for the this behavior is simple: if we explicitly use val (or var), the compiler will create a field with a getter with the same scope as the parameter. If we just have the parameter, a field and internal getter will be created only if the parameter is used outside the constructor scope, for example in a method. For completeness, case classes automatically have the val keyword "appended" to parameters. After what we said it means that when using val, we actually have a field with the given name and the right scope and it will automatically override whatever the trait requires us to do.

Traits as classes

Traits can also be seen from the perspective of classes. In this case, they have to implement all their methods and have only one constructor that does not accept any parameters. Consider the following:

trait Beeper { 
  def beep(times: Int): Unit = { 
    assert(times >= 0) 
    1 to times foreach(i => System.out.println(s"Beep number: $i")) 
  } 
}

Now we can actually instantiate Beeper and call its method. The following is a console application that does just this:

object BeeperRunner { 
  val TIMES = 10 
  
  def main (args: Array[String]): Unit = { 
    val beeper = new Beeper {} 
    beeper.beep(TIMES) 
  } 
}

As expected, after running the application, we will see the following output in our terminal:

Beep number: 1 
Beep number: 2 
Beep number: 3 
Beep number: 4 
Beep number: 5 
Beep number: 6 
Beep number: 7 
Beep number: 8 
Beep number: 9 
Beep number: 10

Extending classes

It is possible for traits to extend classes. Let's have a look at the following example:

abstract class Connector { 
  def connect() 
  def close() 
} 

trait ConnectorWithHelper extends Connector { 
    def findDriver(): Unit = { 
    System.out.println("Find driver called.") 
  } 
} 

class PgSqlConnector extends ConnectorWithHelper { 
  override def connect(): Unit = { 
    System.out.println("Connected...") 
  } 

  override def close(): Unit = { 
    System.out.println("Closed...") 
  } 
}

Here, as expected, PgSqlConnector will be obliged to implement the abstract class methods. As you can guess, we could have other traits that extend other classes and then we might want to mix them in. Scala, however, will put a limit in some cases, and we will see how it will affect us later in this chapter when we look at compositions.

Extending traits

Traits can also extend each other. Have a look at the following example:

trait Ping { 
  def ping(): Unit = { 
    System.out.println("ping") 
  } 
} 

trait Pong { 
  def pong(): Unit = { 
    System.out.println("pong") 
  } 
} 

trait PingPong extends Ping with Pong { 
  def pingPong(): Unit = { 
    ping() 
    pong() 
  } 
} 

object Runner extends PingPong { 
  def main(args: Array[String]): Unit = { 
    pingPong() 
  } 
}

Note

The preceding example is simple and it should really just make the Runner object mix the two traits separately. Extending traits is useful in a design pattern called Stackable Traits, which we will be looking into later in this book.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.218.69