The Cake Pattern is a set of conventions surrounding the use of self type annotations in Scala to do dependency injection. Unlike many other languages, Scala does directly support dependency injection simply by using its built in features. They are very similar to extending a trait to mix in its methods but with some critical differences. This article will explore the features of this language construct and how it is different from inheritance.
Extending a class implies that we inherit its interface and its implementation. When class A extends class B, we say that class A becomes a kind of class B, or in other words we create an is-a relationship. Self-type annotations do not create an is-a relationship. Instead they declare the requirement that when class A is used, class B be mixed in. This will allow us to code class A as if class B’s methods were all locally defined. We can say in a sense that we are mixing class B’s methods into class A’s this, making it available to its own methods, but not into class A’s public interface. This private vs public mixing in is the fundamental difference between the two language features.
Self type annotations is only one of Scala’s language features that can be used for dependency injection. There are other ones, such as implicits and the many patterns surrounding implicits. While implicits allow injection at run time, self-type annotations are more like inheritance in that you set it up as part of your inheritance hierarchy.
What problem do they solve?
Imagine that you are building a large system with many hundreds or thousands of classes. No doubt you will try to find a way to componentize a large class hierarchy like this the best you can, otherwise you won’t be able to maintain it. One of the many ways to do this is to make every piece of functionality a trait. Then when you build a final application component, you put it together by mixing in the methods from these traits.
Using inheritance from abstract traits
Now, when one of these traits needs to use another, you have to somehow declare that in your code. You need at least three layers of traits to understand this: For instance here’s a UserRepository trait which wants to use the getUserList() method from some kind of Dao trait. Then in the next layer of components, a Dashboard trait wants to use our UserRepository trait. You might do something like this:
trait Dao {
def getUserList() : Set[String]
}
trait UserRepository extends Dao {
def printUserInfo(): Unit = {
getUserList()
.foreach(println)
}
}
trait Dashboard extends UserRepository{
def printDashboardHeading(): Unit = {
println("Dashboard:")
}
def printDashboard(): Unit = {
printDashboardHeading()
printUserInfo()
}
}
Doing dependency injection with abstract traits like this will work, in that it will force us to define the methods as we piece the components together, but it has the drawback that now getUserList becomes part of UserRepository’s public interface. Everybody using UserRepository will have to deal with a method that does not make sense to them on that level, because they don’t care about the internal affairs of UserRepository. So in the example above, Dashboard does not want to see getUserList(), because that is part of UserRepository’s private implementation.
You need at least 3 levels of traits to see the benefits
This is where self-type annotations can help. If we replace the extends BaseTrait with this: BaseTrait =>, then those private methods will not inherit down into the public interface of the class, only to its private this. You need three layers to see this, because obviously UserRepository still wants to use getUserList(). It’s just that traits using UserRepository, no longer care to see those methods.
How to set the components up?
Given the above example, we can convert our trait hierarchy into something like this.
trait Dao {
def getUserList() : Set[String]
}
trait UserRepository { this: Dao =>
def printUserInfo(): Unit = {
getUserList()
.foreach(println)
}
}
trait Dashboard { this: UserRepository =>
def printDashboardHeading(): Unit = {
println("Dashboard:")
}
def printDashboard(): Unit = {
printDashboardHeading()
printUserInfo()
}
}
How to use the components?
In order to use these traits we just need to mix them together. We also have to mix in implementations for any abstract traits. Otherwise, we get a compiler error. So we define MangoDao, because Dao was an abstract trait. The following will work, but it has one problem. Can you spot the problem?
trait MangoDao extends Dao{
def getUserList() : Set[String] =
Set("Joe", "Jill", "Marie")
}
class AppDashboard
extends Dashboard
with UserRepository
with MangoDao
object CackeApp extends App{
val dashboard = new AppDashboard
dashboard.printDashboard()
}
Now AppDashboard will have all those implementation methods from Dao and UserRepository that we don’t care to see. We solved the problem for the intermediary components and that is great because there might be thousands of them! But we still failed when making our final component. In large class hierarchies this will mean that we will have thousands of unintended methods in the final app level component’s interface.
We need to create a wrapper class with delegation like the code shows below. This will mix all those methods into a private object, thereby keeping all that mess inside and will only delegate the methods into the public interface that we care about. The Cake Pattern without this final step would not deliver on its main promise of implementation hiding:
class AppDashboard{
private val impl = new Dashboard
with UserRepository
with MangoDao
def printDashboard() =
impl.printDashboard()
}
object CackeApp extends App{
val dashboard = new AppDashboard
dashboard.printDashboard()
}
Conclusion
As explained with the above example, The Cake Pattern is about implementation hiding in large class hierarchies, where we mix in implementations from many traits to build final concrete components to use. In these cases, we want to reduce the visibility of implementation methods, so that they would only be visible where we care to see them. It requires a fair amount of boilerplate which is probably not worth it for small class hierarchies, certainly not below 3 levels of inheritance. But for deep hierarchies it has the promise of making class public interfaces cleaner and smaller on each level.