
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.
Last updated: July 29, 2024
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.
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.
In the stackable trait pattern, we use traits or classes that can play three different roles:
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:
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.
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.
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.