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

Mixin compositions

Scala allows developers to extend many traits in a single class. This adds the possibility of achieving multiple inheritance and save a lot of efforts in code writing, which has to be performed in languages where extending many classes is not allowed. In this sub topic, we will show how traits can be mixed in a specific class or used to create anonymous classes with some specific functionality while writing our code.

Mixing traits in

First of all, let's modify the code from the last example. It is a really simple change and it will also show exactly how traits can be mixed in:

object MixinRunner extends Ping with Pong { 
  def main(args: Array[String]): Unit = { 
    ping() 
    pong() 
  } 
}

As it can be seen from the preceding code, we can add multiple traits to a class. We've used object in the example just because of the main method. This would be similar to creating a class with no constructor parameters.

Tip

How to mix traits in?

Mixing traits into a class is done with the following syntax: extends T1 with T2 with … with Tn.

If a class already extends another class, we just keep on adding the traits using the with keyword.

If a trait method is not implemented inside the trait body and the class we are mixing it into is not abstract, the class will have to implement the trait. Otherwise, a compilation error will occur.

Composing

Composing at creation time gives us an opportunity to create anonymous classes without the need to explicitly define them. Also, if there are many different traits that we might want to combine, creating all the possibilities would involve too much work. Wouldn't it?

Composing simple traits

Let's see an example where we compose simple traits, which do not extend other traits or classes:

class Watch(brand: String, initialTime: Long) { 
  def getTime(): Long = System.currentTimeMillis() - initialTime 
} 
 
object WatchUser { 
  def main(args: Array[String]): Unit = { 
    val expensiveWatch = new Watch("expensive brand", 1000L) with Alarm with Notifier { 
      override def trigger(): String = "The alarm was triggered." 
      override def clear(): Unit = { 
        System.out.println("Alarm cleared.") 
      } 
      override val notificationMessage: String = "Alarm is running!" 
    } 
    val cheapWatch = new Watch("cheap brand", 1000L) with Alarm { 
      override def trigger(): String = "The alarm was triggered." 
    } 
    // show some watch usage. 
    System.out.println(expensiveWatch.trigger()) 
    expensiveWatch.printNotification() 
    System.out.println(s"The time is ${expensiveWatch.getTime()}.") 
    expensiveWatch.clear() 
    
    System.out.println(cheapWatch.trigger()) 
    System.out.println("Cheap watches cannot manually stop the alarm...") 
  } 
}

In the preceding example, we used the Alarm and Notifier traits from before. We created two watch instances—one is expensive that has more functionality and is more useful, and the other one is a cheap one that does not give too much control. Essentially, they are anonymous classes, which are defined during instantiation. Another thing to note is that, as expected, we have to implement the abstract methods from the traits we include. Hope this gives you an idea of how many combinations there might be in the cases where we have more traits.

Just for the sake of completeness, here is an example output of the preceding program:

The alarm was triggered.
Alarm is running!
The time is 1234567890562.
Alarm cleared.
The alarm was triggered.
Cheap watches cannot manually stop the alarm...

As expected, the highlighted time value will be different in the different runs.

Composing complex traits

It is possible that in some cases, we would have to compose more complex traits, which extend other traits or classes. If a trait and no other trait up the inheritance chain extends a specific class explicitly, then things will be pretty simple and they don't change much. In this case, we would simply have access to the methods from the super traits. However, let's see what happens if any of the traits in the hierarchy extend a specific class. For the next example, we will be using the ConnectorWithHelper trait defined previously. This trait extends the abstract Connector class. Imagine that we want to have another really expensive watch, which can also connect to a database:

object ReallyExpensiveWatchUser { 
  def main(args: Array[String]): Unit = { 
    val reallyExpensiveWatch = new Watch("really expensive brand", 1000L) with ConnectorWithHelper { 
      override def connect(): Unit = { 
        System.out.println("Connected with another connector.") 
      } 
      override def close(): Unit = { 
        System.out.println("Closed with another connector.") 
      } 
    } 
    
    System.out.println("Using the really expensive watch.") 
    reallyExpensiveWatch.findDriver() 
    reallyExpensiveWatch.connect() 
    reallyExpensiveWatch.close() 
  } 
}

It seems that everything is fine; however, when we compile, we get the following error message:

Error:(36, 80) illegal inheritance; superclass Watch 
 is not a subclass of the superclass Connector 
 of the mixin trait ConnectorWithHelper 
    val reallyExpensiveWatch = new Watch("really expensive brand", 1000L) with ConnectorWithHelper { 
                                                                           ^

This error message tells us that since the ConnectorWithHelper trait extends the Connector class, all the classes that use this trait for composition must be subclasses of Connector. Let's now imagine that we wanted to mix in another trait that also extends a class, but a different one in this case. According to the preceding logic, it will be required that Watch should also be a subclass of the other class. This, however, wouldn't be possible, as we can only extend one class at a time and this is how Scala limits multiple inheritances in order to prevent dangerous errors from happening.

If we want to fix the compilation issue in the example, we will have to modify the original Watch class and make sure it is a subclass of Connector. This, however, might not be desired and some refactoring might be needed in such cases.

Composing with self types

In the previous subsection, we saw how we were forced to extend Connector in our Watch class in order to properly compile our code. There are cases where we might actually want to enforce a trait to be mixed into a class that also has another trait or multiple traits mixed into it. Let's imagine that we want to have an alarm that must be able to notify, no matter what:

trait AlarmNotifier {
 this: Notifier =>

  

  def trigger(): String
}

In the preceding code, we've shown a self-type. The highlighted piece of code brings all the methods of Notifier to the scope of our new trait and it also requires that any class that mixes in AlarmNotifier should also mix in Notifier. Otherwise, a compilation error will occur. Instead of this, we can use self and then refer to the Notifier methods inside AlarmNotifier by typing, for example, self.printNotification().

The following code is an example of how to use the new trait:

object SelfTypeWatchUser {
  def main(args: Array[String]): Unit = {
    // uncomment to see the self-type error. 
    // val watch = new Watch("alarm with notification", 1000L) with AlarmNotifier {
    //}

    val watch = new Watch("alarm with notification", 1000L) with AlarmNotifier with Notifier {
      override def trigger(): String = "Alarm triggered."

      override def clear(): Unit = {
        System.out.println("Alarm cleared.")
      }

      override val notificationMessage: String = "The notification."
    }
    
    System.out.println(watch.trigger())
    watch.printNotification()
    System.out.println(s"The time is ${watch.getTime()}.")
    watch.clear()
  }
}

If we comment out the watch variable in the preceding code and uncomment the commented bit, we will see a compilation error that is raised due to the fact that we must also mix Notifier in.

In this subsection, we showed a simple use of self-types. One trait can require multiple other traits to be mixed in. In such cases, they are just separated with the with keyword. Self-types are a key part of the cake design pattern, which is used for dependency injection. We will see more interesting use cases later in the book.

Clashing traits

Some of you might already have a question in your mind—What if we mix in traits that have methods with identical signatures? We will look into this in the next few sections.

Same signatures and return types traits

Consider an example where we want to mix two traits into a class and their declaration of a method is identical:

trait FormalGreeting { 
  def hello(): String 
} 

trait InformalGreeting { 
  def hello(): String 
} 

class Greeter extends FormalGreeting with InformalGreeting { 
  override def hello(): String = "Good morning, sir/madam!" 
} 

object GreeterUser { 
  def main(args: Array[String]): Unit = { 
    val greeter = new Greeter() 
    System.out.println(greeter.hello()) 
  } 
}

In the preceding example, the greeter is always polite and mixes both formal and informal greetings. While implementing, it just has to implement the method once.

Same signatures and different return types traits

What if our greeting traits had more methods that have the same signatures, but a different return type? Let's add the following declaration to FormalGreeting:

def getTime(): String

Also, add the following to InformalGreeting:

def getTime(): Int

We will have to implement these in our Greeter class. However, the compiler will not allow us with the message that getTime was defined twice, which shows that Scala prevents such things from happening.

Same signatures and return types mixins

Before going further, a quick reminder that a mixin is just a trait that has some code implemented inside. This means that in the following examples, we do not have to implement the methods inside the class that uses them.

Let's have a look at the following example:

trait A { 
  def hello(): String = "Hello, I am trait A!" 
} 

trait B { 
  def hello(): String = "Hello, I am trait B!" 
} 

object Clashing extends A with B { 
  def main(args: Array[String]): Unit = { 
    System.out.println(hello()) 
  } 
}

Probably as expected, our compilation will fail with the following message:

Error:(11, 8) object Clashing inherits conflicting members: 
  method hello in trait A of type ()String  and 
  method hello in trait B of type ()String 
(Note: this can be resolved by declaring an override in object Clashing.) 
object Clashing extends A with B { 
       ^

The message is useful and it even gives us a hint about how to fix the problem. Clashing methods is a problem in multiple inheritances, but as you can see, we are forced to pick one of the available methods. Here is a possible fix inside the Clashing object:

override def hello(): String = super[A].hello()

However, what if we want to use both the hello methods for some reason? In this case, we can create other methods that are named differently and call the specific traits as in the preceding example (the super notation). We can also directly refer to the methods with the super notation instead of wrapping them in a method. However, my personal preference would be to wrap it, as the code could get messy otherwise.

Tip

The super notation

What would happen if in the preceding example, instead of super[A].hello(), we do the following: override def hello(): String = super.hello() Which hello method will be called and why? In the current case, it will be the one in the B trait and the output will be Hello, I am trait B! This depends on linearization in Scala, and we will be looking into this later in this chapter.

Same signatures and different return types mixins

As expected, the previous problem does not exist when input parameters to the methods differ either by type or by count since this is a new signature. However, the problem will still be there if we have the following two methods in our traits:

def value(a: Int): Int = a // in trait A 
def value(a: Int): String = a.toString // in trait B

You will be surprised to see that the approach we used will not work here. If we decide to override only the value method in the A trait, we will get the following compilation error:

Error:(19, 16) overriding method value in trait B of type (a: Int)String; 
 method value has incompatible type 
  override def value(a: Int): Int = super[A].value(a) 
               ^

If we override the value method in the B trait, the error will change respectively.

If we try and override both, then the error will be this:

Error:(20, 16) method value is defined twice 
  conflicting symbols both originated in file '/path/to/traits/src/main/scala/com/ivan/nikolov/composition/Clashing.scala' 
  override def value(a: Int): String = super[B].value(a)

This shows that Scala actually prevents us from doing some dangerous things that can occur in multiple inheritance. For the sake of completeness, if you face a similar issue, there is a workaround (by sacrificing the mix in functionality). It will look like the following:

trait C { 
  def value(a: Int): Int = a 
} 

trait D { 
  def value(a: Int): String = a.toString 
} 

object Example { 
  
  val c = new C {} 
  val d = new D {} 
  
  def main (args: Array[String]): Unit = { 
    System.out.println(s"c.value: ${c.value(10)}") 
    System.out.println(s"d.value: ${d.value(10)}") 
  } 
}

The preceding code uses traits as collaborators, but it also loses the fact that the class that uses them is also an instance of the trait type, which can be useful for other operations as well.