Baeldung Pro – Scala – NPI EA (cat = Baeldung on Scala)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Introduction

The stackable trait pattern is a Scala design pattern based on the composition of traits (aka mixins).

In this tutorial, we will learn about mixins and how to use them to create stackable modifications of a Scala trait or class.

2. Mixins

In a few words, mixins are Scala traits that provide another Scala class/trait with some behavior. Let’s look at a simple example:

trait Person {
  val name: String
  val country: String
}

case class ItalianPerson(name: String) extends Person {
  val country = "Italy"
}

trait WithPrettyPrinting extends Person {
  def prettyPrint: String =
    s"""Name: $name
      |Country: $country""".stripMargin
}

@main
def main(): Unit =
  val italian = new ItalianPerson("Mario") with WithPrettyPrinting

  println(italian)
  println(italian.prettyPrint)

In the example above, we first defined a trait named Person, with simple details about a person, such as name and country of birth. Then, we defined a case class for an Italian person, where only the name is a parameter in the constructor. Lastly, we implemented another trait, WithPrettyPrinting, adding a prettyPrint method. The latter extends Person to access the name and country properties.

In the main method, we instantiated a value named italian, which mixins the WithPrettyPrinting trait. In Scala, a class can only have one superclass (Person in this case) but can have several mixins.

If we run the example above, we’ll get:

ItalianPerson(Mario)

Name: Mario
Country: Italy

In the output above, the call to ItalianPerson::toString prints the first line, whereas the latter two are the consequence of the call to ItalianPerson::prettyPrint.

In Scala, mixins are a powerful feature to add features to an existing class/trait. As we’ll see shortly, they also enable the stackable trait pattern.

3. The Stackable Trait Pattern

In the stackable trait pattern, we use traits or classes that can play three different roles:

  • Base trait/abstract class, defining the interface core traits will extend
  • Core trait/class, implementing the interface defined by base traits to provide different functionality. They are concrete components; that is, they implement the entire base interface
  • Stackable trait, overriding one (or more) methods of the base trait and invoking the super implementation of the same method. This way, we can stack behaviors on top of core traits

3.1. Stackable Trait Example

Let’s see an example of the stackable trait pattern. We’ll model a simple hierarchy to apply transformations to an Int:

trait IntTransformation {
  def transform(value: Int): Int = value
}

trait DoubleTransformation extends IntTransformation {
  abstract override def transform(value: Int): Int =
    super.transform(value * 2)
}

trait LogInt extends IntTransformation {
  abstract override def transform(value: Int): Int = {
    println(s"Transforming value: $value")
    super.transform(value)
  }
}

trait CustomTransformation(f: Int => Int) extends IntTransformation {
  abstract override def transform(value: Int): Int =
    super.transform(f(value))
}

In the snippet above, we first defined our base trait, IntTransformation. As we said above, it is supposed to declare the base interface. In this case, that is a simple transform method, inputting an Int and returning a transformed Int. In our case, IntTransformation acts as a base and core trait, as it implements the interface with the identity transformation.

Then, we implement three other traits, the stackable ones:

  • DoubleTransformation doubles the number
  • LogInt logs the number
  • CustomTransformation inputs a user-defined function and applies it to the number

All the stackable traits always call super.transform() at some point. This forms a chain of calls that goes up the stack until it reaches the core trait.

Let’s now see how to use it:

val logAndDouble = new IntTransformation
    with DoubleTransformation
    with LogInt {}
val doubleAndLog = new IntTransformation
    with LogInt
    with DoubleTransformation {}
val logAndCustom = new IntTransformation
    with CustomTransformation(_ + 1)
    with LogInt {}

println(s"Log and double: ${logAndDouble.transform(5)}")
println(s"Double and log: ${doubleAndLog.transform(5)}")
println(s"Log and increment: ${logAndCustom.transform(5)}")

The snippet above defines three stacks, playing with the stackable traits we saw above. The order here is important. When a call to transform takes place, Scala will first call the method of the rightmost trait. For example, in logAndDouble, LogIn::transform will get called before DoubleTransformation::transform.

If we run the example above, it will print:

Transforming value: 5
Log and double: 10

Transforming value: 10
Double and log: 10

Transforming value: 5
Log and increment: 6

logAndDouble.transform(5) prints the first two lines. As we saw above, first LogInt prints 5, and then DoubleTransformation doubles it, returning 10, which is the final result. doubleAndLog.transform(5) does the opposite: 5 is multiplied by 2 as a first step, and LogInt prints it.

4. Similarities with the Decorator Design Pattern

The stackable traits represent modifications to the behavior of the core trait or class. In particular, instead of writing multiple Transformation classes for the different use cases, we implement different behaviors and compose them stack-based.

The idea behind this is quite similar to the decorator design pattern. As a matter of fact, in the decorator pattern, we make use of object composition to modify the core behavior at run time using the same trick. Let’s see how we could rewrite our previous example to match the decorator structure:

trait IntTransformation {
  def transform(value: Int): Int = value
}

class TransformationDecorator(wrappee: IntTransformation)
  extends IntTransformation {
  override def transform(value: Int): Int = wrappee.transform(value)
}

class DoubleDecorator(wrappee: IntTransformation)
  extends TransformationDecorator(wrappee) {
  override def transform(value: Int): Int =
    super.transform(value * 2)
}

class LogInt(wrappee: IntTransformation) 
extends TransformationDecorator(wrappee) { override def transform(value: Int): Int = { println(s"Transforming value: $value") super.transform(value) } } class CustomDecorator(f: Int => Int, wrappee: IntTransformation) extends TransformationDecorator(wrappee) { override def transform(value: Int): Int = super.transform(f(value)) }

The structure is, more or less, similar to before. Out IntTransformation trait now defines the basic implementation. Then, BaseDecorator implements the core of the decorator pattern. It uses inheritance and composition over IntTransformation to get its interface and a concrete implementation of it (wrappee). BasicDecorator::transform delegates the call to the wrappee.

Then, all the other classes extend TransformationDecorator and modify the behavior of the transform method as before. Let’s see how we use it:

val identity = new IntTransformation {}

val withLogging = new LogInt(identity)
val withDouble = new DoubleDecorator(withLogging)
val withCustom = new CustomDecorator(_ + 1, withDouble)

println(s"With increment, double, and logging: ${withCustom.transform(5)}")

In this case, we decorated the basic identity IntTransformation with three other behaviors. First, the number gets incremented by one, doubled, and then logged. With this pattern, behavioral changes take place bottom-up rather than from right to left, but the idea is the same.

5. Conclusion

In this article, we examined the stackable trait pattern in Scala. First, we uncovered its foundations and analyzed the trait mixins. Second, we learned the pattern’s components and saw it in action with a practical example. Lastly, we compared it with the decorator pattern.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.