Scala Design Patterns
上QQ阅读APP看书,第一时间看更新

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.