Scala Tutorial - Trait Mixin And Linearization
Overview
In this tutorial, we will learn about the mixin concept when using traits. At the same time, we will also put emphasis, and illustrate, the importance of linearization when it comes to using traits. It goes without saying that mixin as opposed to object inheritance favors composition and, as such, can be thought of to provide you with greater flexibility, as opposed to the classical approach of inheriting some features from a given base class. As its name implies, mixin is the process of putting or gluing together a number of traits in order to benefit from the features that are exposed from these particular traits. Linearization, on the other hand, represents the ordering, or sequencing, of the corresponding traits. If none of these make any sense at the moment, that's OK! As always, we'll provide step-by-step guide to the concepts.
As a reminder, and before proceeding further, feel free to review the previous materials on traits as they are especially important:
- Learn How To Create And Extend Trait
- Learn How to Create Trait With Type Parameters
- Learn How To Extend Multiple Traits
- Learn How to Use Traits for Dependency Injection Part 1
- Learn How To Use Trait For Dependency Injection Part 2 Avoid Cake Pattern
- Traits, Companion Objects, Factory Pattern
- Type Class For Ad-hoc Polymorphism
Steps
1. Review of inheritance using traits
We begin with a quick review of inheritance using traits, and create a trait ShoppingCart[T] which defines an abstract printItems() method. We then create a class DonutShoppingCart that extends the trait ShoppingCart, and provides the necessary implementation for the printItems() method. In addition, we’ve used a case class to represent a basic Donut type with a name property of type String as our domain, or business, object.
println("Step 1: Review of inheritance using traits")
case class Donut(name: String)
trait ShoppingCart[T] {
def printItems(items: Seq[T]): Unit
}
class DonutShoppingCart extends ShoppingCart[Donut] {
override def printItems(items: Seq[Donut]): Unit = {
items.foreach(println(_))
}
}
Without any surprises, we create an object, or instance, of DonutShoppingCart using the new keyword, and thereafter invoke its printItems() method.
val donuts = Seq(Donut("PlainDonut"), Donut("VanillaDonut"))
val donutCart = new DonutShoppingCart()
donutCart.printItems(donuts)
You should see the following output when you run your Scala application in IntelliJ:
Step 1: Review of inheritance using traits
Donut(Plain Donut)
Donut(Vanilla Donut)
2. Define trait PrettyPrintUpperCase and mixin with DonutShoppingCart
In your day-to-day programming within a real-world context, you are bound to deal with ever changing requirements! For instance, what if the printItems() method should output all items in the Sequence using capital letters?
With mixin and composition in mind, you can easily extend the trait ShoppingCart with a new trait PrettyPrintUpperCase[T]. Thereafter, you can use the with keyword to mixin the latter trait with a particular object, or instance, of DonutShoppingCart - that is, new DonutShoppingCart() with PrettyPrintUpperCase[Donut]. On a side note when working with large enterprise code bases, it is typical for various parts of a given platform to be developed in parallel. To this end, and with the above approach, it would have been fairly straightforward for another developer, or team, to work on the trait PrettyPrintUpperCase. In essence, we were able to modify the behavior of the printItems() method without editing the original class DonutShoppingCart.
println("\nStep 2: Define trait PrettyPrintUpperCase and mixin with DonutShoppingCart")
trait PrettyPrintUpperCase[T] extends ShoppingCart[T] {
override def printItems(items: Seq[T]): Unit = items.foreach(items => print(item.toString.toUpperCase))
}
val donutCart2 = new DonutShoppingCart() with PrettyPrintUpperCase[Donut]
donutCart2.printItems(donuts)
You should see the following output when you run your Scala application in IntelliJ:
Step 2: Define trait PrettyPrintUpperCase and mixin with DonutShoppingCart
DONUT(PLAIN DONUT)
DONUT(VANILLA DONUT)
3. Linearization when mixin multiple traits
The Scala Specification provides an in-depth explanation with regards to linearization when mixin is used with multiple traits. We’ll put this into context here by creating another trait PrettyPrintLowerCase[T], which also extends the base trait ShoppingCart[T]. Let us observe what happens when we mixin an object, or instance, of DonutShoppingCart with PrettyPrintUpperCase[Donut], and followed with PrettyPrintLowerCase[Donut].
println("\nStep 2: Define trait PrettyPrintUpperCase and mixin with DonutShoppingCart")
trait PrettyPrintLowerCase[T] extends ShoppingCart[T] {
override def printItems(items: Seq[T]): Unit = items.foreach(items => print(item.toString.toLowerCase))
}
val donutCart3 = new DonutShoppingCart() with PrettyPrintUpperCase[Donut] with PrettyPrintLowerCase[Donut]
donutCart3.printItems(donuts)
You should see the following output when you run your Scala application in IntelliJ:
Step 3: Linearization when mixin multiple traits
donut(plain donut)
donut(vanilla donut)
From the above output, we notice that the items in the donuts Sequence are printed in small letters. Similarly, let us observe what happens when we mixin an object, or instance, of DonutShoppingCart with PrettyPrintLowerCase[Donut], and followed with PrettyPrintUpperCase[Donut].
val donutCart4 = new DonutShoppingCart() with PrettyPrintLowerCase[Donut] with PrettyPrintUpperCase[Donut]
donutCart4.printItems(donuts)
You should see the following output when you run your Scala application in IntelliJ:
DONUT(PLAIN DONUT)
DONUT(VANILLA DONUT)
In this case, the items in the donuts Sequence are printed in capital letters. That being so, you should bear in mind that with the linearization of traits, the order, or sequence, of mixin matters!
Summary
In this tutorial, we went over the following:
- Review of inheritance using traits
- Define trait PrettyPrintUpperCase and mixin with DonutShoppingCart
- Linearization when mixin multiple traits
Tip
- Now that we’ve covered linearization when using mixin, you can most certainly guess that in large enterprise code base with multiple services, or layers, you should pay special attention to the order of mixin. That is especially true if you are in fact using the Cake Pattern, or other equivalent design.
Source Code
The source code is available on the allaboutscala GitHub repository.
What's Next
In the next tutorial, I will show you how to define and use The Magnet Pattern.