Cake is often considered as the most idiomatic way to do Dependency Injection in Scala. Yet surprisingly, until now nobody (to my knowledge) has stated publicly, that it is actually a composite pattern, and that DI is only one part of it.
The result is that people, who just want DI apply Cake pattern with its full complexity. Usually that complexity is totally unnecessary and only makes code harder to understand.
Some people have already noticed, that something is wrong with that. I've also had a gut-feeling, that something is a bit too clever for me, when we were starting a messaging gateway project for GSM operator in Scala. Therefore we've sticked with old-fashioned yet proven manual constructor injection, something along the lines of DIY-DI. Although people expressed their criticism of the pattern, nobody stated precisely what the problem was, though. Neither me, until recently. Just few days ago it struck me. I've done a little research, re-read the original paper, and got it. Let me slice that Cake to its constituent layers to see, what it has inside. Then you will clearly see what's the problem with that pattern's common usage.
Self-types as a mean to inject dependencies
What is the heart of Dependency Injection? With DI, components neither lookup nor create their own collaborators. They simply declare, what are their dependencies and rely on external code (usually some framework or manual factory) to provide that dependencies for them. Component can declare its dependencies in many ways, chosen by developer and constrained by DI framework used. For example dependencies can be injected via constructor, via setters, via private field. The actual injection is either done manually (in a factory class), or via framework, that usually is configured via a mix of annotations, XML, plain code and some defaults.
The main benefit of so-achieved DI is freedom to independently test individual components in isolation. As a bonus you also get an explicit view of all components' dependencies and gain an order in component lifecycles, not to mention some frameworks' additional benefits, like AOP.
Scala adds one more method for a component to declare its dependencies: it's self-type annotations.
Technically, declaring a trait's (let's say BuyerAgent) self-type as (let's say) WebCrawler is similar to declaring all members of WebCrawler in this trait. The self-type (in this case type WebCrawler) becomes the dependency of our BuyerAgent, because now we can refer to WebCrawler's members from our BuyerAgent's methods.
trait WebCrawler {
def goTo(target: URL)
def pageSource: String
}
trait BuyerAgent { this: WebCrawler =>
def buyItem() = {
// all WebCrawler members are accessible
// thanks to declared self-type
goTo(new URL("http://ebay.com"))
val ps = pageSource
// if item price is ok, click "buy"
}
}
Note, that this dependency is in a sense more intimate, than one declared (let's say) via constructor parameter and kept in a private field. Our example trait does not HAVE its WebCrawler accessible via a field, it IS the WebCrawler. In practice, this enables you to skip the references to WebCrawler field when using WebCrawler's features (because there is no field in the first place).
/* Traditional, constructor-based DI
(note only classes can have constructors) */
class BuyerAgent(crawler: WebCrawler) {
def buyItem() = {
// all references to WebCrawler members
// are explicit
crawler.goTo(new URL("http://ebay.com"))
val ps = crawler.pageSource
// ...
}
}
This is both good and bad in my opinion. It's best, when dependencies declared in that way really model parts or layers of that component (trait). Used this way, self-types allow you to conveniently split one, big component into many independently-testable and replacable layers, at the same time stressing the tight connection between them. Extracting them to separate classes would make them look as more independent, than they are. Though in my opinion, self-types can be also abused, when component uses it to obtain a dependency, that certainly IS NOT its part. That is pretty much a matter of taste probably.
So going back to Cake, let's look at it. What does it have to offer in terms of DI? Idiomatic usage of self-type. What other Cake's features do you need to implement DI? Absolutely none. If you applied the whole Cake only in order to get DI, you can safely remove Cake's other elements. Your code will immediately get more clarity and simplicity with benefit for you and your team.
So, what are other Cake's constructs (like nested types) for?
Two things.
Unwanted interchanging component's parts prevention.
First, nested classes in Scala work slightly differently than in Java. If you create more than one instance of your component, you won't be able to interchange their inner parts. For example, if your component has inner class named Part, you won't be able to take a single Part from it and pass it to your second component's method (even if that would be no problem in Java, since the types look the same). Such constraint has been enforced in the Scala compiler code. If you can benefit from such protection, Cake's nested classes are for you. Personally I haven't seen a need to use that yet (I see no problem in mixing inner parts between different instances), though I would be interrested to see a case, when presence of that feature makes a real difference. Maybe in Scala compiler code it really did, I don't know. It just doesn't attract me much enough to make me find it out for myself.
Modeling families of types that vary together covariantly
Second, nested classes have access to all members of enclosing trait. If you move them outside the enclosing trait, access to that members is lost. In the original paper, two nested classes of SubjectObserver type refer to its common type members - type of Subject and type of Observer. Both type members have variance annotation. This way, when you extend SubjectObserver refining type members, your updated types immediately propagate to both nested classes and compiler helps you get the variance right. That lets you enforce some domain constraints when you design reusable components for others. In the SubjectObserver example, that forces all extending types to refer to each other in a consistent manner.
So besides of DI, we got two distinct features of Cake pattern. Each of them allows us to enforce some design decisions with the help of compiler. In one case, this is increased integrity of component instances, in second, ability to model type families, that change together. None of that features is so widely popularized like the Dependency Injection aspect and probably they deserve a better exploration.
Nevertheless, Cake is a composite pattern and DI constitues just a part of it. If you consider using it to get DI, just use only self-types and you'll be better off. Nested classes' part of Cake will only obscure your code, when you don't plan to leverage their specific features. As you can see, you probably aren't going to need them most of the time, so it's much better to keep things simple.
Why is the pattern most often applied in its full form, when usually only DI matters? I think, that many factors come into play here. Many new concepts at once after coming from Java, insufficiently thourough understanding. Whatever the reaons are, usually we are much better off sticking to the KISS rule, though :)
Originally I planned to add code samples, that could easily show similarities and differences between DI implemented with Cake pattern and with plain, old, constructor injection. Because I'm kind of new to blogging and don't want to spend additional time integrating syntax highlighting now, I'm publishing this post without it. If you are interrested I will add them.
Please share your thoughts on this topic, I'll gladly learn something new and discover new points of view. BTW, if you are native English speaker, I'll appreciate any corrections of my sometimes odd grammar and vocabulary ;) Thanks!