2014-12-15

Deconstructing the Cake Pattern

This article provides a technical comparison between using the cake pattern and constructor injection for doing dependency injection in Scala. I show that within a small handful of refactoring steps, we can migrate from the cake pattern to constructor injection. We lose one minor feature along the way. The lost feature is not terribly desirable anyway, and can be emulated with constructor injection if need be. In exchange, we jettison the boilerplate and the opaque nature of the cake pattern.


Earlier this year I went to a talk by Adam Warski on his dependency injection framework MacWire. (I saw the talk at the Boston Area Scala Enthusiasts meetup, but he gave the same talk at ScalaDays 2014, which you can watch here on Parlays.) He uses a constructor injection approach, and makes use of def macros to remove some of the boilerplate involved with doing this kind of dependency injection. It was a very good talk, and got me thinking a bit about all the complexities of using the cake pattern. And it gave me an idea for a hybrid approach of using the cake pattern along with constructor injection that would, among other things, make the cake pattern code easier to understand. Following through with this experiment, I found myself one step away from constructor injection - all I had to do was remove some intermediary classes that were not providing much utility anyway. In other words, remove some boilerplate.

A Domain Model for a Blog

We're going to be working with a semi-realistic example here, to make things a bit more interesting and easy to follow. Let's say we are working on a blogging application, and our domain model includes entities such as BlogPost, and Author. Both a Blog and a Post can have multiple Authors, and a Post belongs to a single Blog. We choose to model these relationships as unidirectional associations, like so:


We can stub out this domain model in Scala like so:

You'll notice that we haven't explicitly included any associations in our domain model. We'll save that as a responsibility for our repositories and services.

A Repository Layer

Each one of our domain entities has a corresponding repository class to handle persistence operations for the entity. This will handle the standard CRUD operations, as well as retrieving associations between entities. For instance, the repository for Posts might look something like this:


Because the PostRepo is responsible for looking up the Blog and Authors for a Post, an implementation of this class will have dependencies on the BlogRepo and the AuthRepo. A class diagram for these repositories might look like this:


A Service Layer

Now let's add our service layer. Each of our entity classes has a service class to manage business logic related to that entity type. Each one has a dependency on the corresponding repository, expanding the above figure to this:


The outer application layers are intended to interact only with the service layer. The repository layer is, at least conceptually, hidden from them. Something like this:


Here, the application layer is responsible for handling user requests, and translating data and information from the domain model into the various presentation models used in the UI. But we'll leave off the application layer for the remainder of this post, and focus on the repositories and services.

Using the Cake Pattern

Let's do a quick review of the typical usage of the cake pattern as a starting point, so we can point out some of the problems with the pattern, and consider how we might improve things. This is not an introduction to the cake pattern, so I won't be going into all of the details of why things are done the way they are. There are many good resources available that will give you an introduction to the cake pattern, including my original cake pattern post, which goes into great detail.

Let's start with our AuthRepo. It provides basic CRUD functionality for persisting and retrieving Author entities to and from the database. There are two important things to note here. First, we combine the repository itself, (the service), with an injection point for that service, into a component. Second, we want to be able to swap in alternative implementations for our component, so we start out with a trait where we define the API for our service and injection point. Here's how it looks:

Our next step is to define our default implementation. Instead of actually interacting with the database, we'll just put printlns in the method bodies, so we can see when they are called. Notice that we provide implementations for both the repository class as well as the injection point.

Now let's define our AuthServ, which in early versions of our application, has methods that simply mirror the methods in our AuthRepo. In future iterations of our application, the AuthServ will begin to contain some actual business logic:

Now our default implementation of the AuthServ has a dependency on the AuthRepo. We express with with a self-type, which means that by the time our AuthServComponentImpl is actually instantiated, it will also be an AuthRepoComponent. Because this is true, we can access the authRepo injection point from within the AuthServComponentImpl. It ends up looking like this:


After going through similar steps for the repositories and services for our Blog and Post entities, we combine all our components into a top-level component:


Now our application can extend this component, and be able to access all the injection points:


But note our Application is still abstract. If we tried to instantiate it as-is, we would get compiler errors about all our injection points being abstract. So we provide a default implementation of our TopLevelComponent like so:


Now we can instantiate our Application as follows:


Drawbacks of Using the Cake Pattern

I've talked about many of the drawbacks of using the cake pattern in earlier posts, including in my first cake pattern post, Component Based Dependency Injection in Scala. The two major drawbacks I discussed there were (1) too much boilerplate, and (2) too difficult to understand. Here is a handful of other, more subtle problems:
  1. The method used by a service class to access the dependency is confusing. We actually have to reach out into an enclosing scope to find the dependency. It's a strain to have to think about the enclosing scope when writing a service class. I should be able to reason about the details of the service class independently of the dependency injection framework.
  2. The simple fact that my service class is not a top-level class, makes the service code harder to read and write. There is an extra few lines of boilerplate before I reach my actual service code, and all the service code is indented an extra level.
  3. Incremental compilation is thwarted, since a change to a service forces a recompile of its containing component, all components that depend on that component, the top-level component, and any classes that inherit from the top-level component.
All of these problems arise from a failure to separate concerns. In particular, our dependency injection framework code is interleaved with the code of our services. This presents special difficulties because of the advanced language features used to do dependency injection in the cake pattern. Why should the writer of a service class have to understand self-types to figure out how to access the service dependencies?

A Hybrid Solution

To resolve these problems, let's declare all our services and repositories - both the abstract traits and default implementations, as top-level classes. We'll provide the dependencies for the services using constructor injection:


Wow, now we can work on our service classes without thinking about the dependency injection framework at all! Now that we have moved these classes to the top-level, the cake pattern code can be expressed separately:


The TopLevelComponentTopLevelComponentImpl, and Application classes presented above do not need to change for our hybrid solution.

I don't know about you, but personally, it's much easier to see what is going on with these component classes, how dependencies are expressed, etc., without all the clutter of the actual service traits and classes inside of them. As we shall see, this separation-of-concerns step ends up making it easier to identify and remove some cruft. But before we go there, let's assess what we've gained by taking this step.

Benefits of Decoupling

Decoupling the service classes and traits themselves from the component classes provides us with many benefits. For one thing, this is a true decoupling in the sense that there are fewer compilation dependencies between our classes. For instance, suppose we were to make a change to AuthServ and/or AuthServImpl. None of the component classes would need to be recompiled. The inverse is also true: any change to our component hierarchy will not force a recompilation of any of our services. This will be a great advantage to incremental compilation, which plays a strong role in standard IDEs such as Eclipse and IDEA, as well as in SBT.

Of course, the best benefits from this decoupling are for the programmer who has to read, write, and modify this code. The fact that the AuthServImpl has a dependency on the AuthRepo has become an entirely local phenomenon from the perspective of the AuthServImpl. And that dependency is expressed in an entirely standard and easy-to-understand way: constructor injection.

Likewise, the dependency framework code is isolated from the details of our service classes. This allows us to reason about the complicated self-types and inheritance patterns used by the cake pattern in isolation. It's much easier to reason about these constructs in such short classes, uncluttered by the service APIs and implementations. In fact, these classes are so short, that you could stick your whole dependency injection framework in just one or two source files. The the programmer would be able to view the skeleton of the dependency framework in a single window. Reasoning about the relationships between these components would benefit from being able to view them all on the screen at the same time - or by paging up and down in a single file.


One Step from Constructor Injection

So I had gotten to this point in my modified cake pattern experiment, when it occurred to me that we were not very far from constructor injection at all. We simply have to apply a collapse hierarchy refactoring about a dozen times, merging traits like AuthServComponent into TopComponent, and traits like AuthServComponentImpl into TopComponentImpl. Following through with this, we would replace the fourteen component traits presented above with the following two:


And we end up with a textbook example of constructor injection. At this point, we have to ask ourselves, why are we using the cake pattern again? As you can see, the cake pattern comes with 12 extra types, defined along the lines of this:

Now don't me wrong, I love strong typing, and I especially love the strong and flexible typing found in Scala. But still, every type we create ought to have a purpose. The AuthServComponent defines a single injection point, authServ. But what use is a type that defines a single injection point? A dependency injection framework only becomes useful when we start injecting dependencies, and that takes two. The AuthServComponentImpl type declares that the authServ has a dependency on the authRepo, but isn't this apparent from the signature of the constructor of the AuthServ?

Mutually Dependent Services

After seeing the comparison between constructor injection and our modified cake pattern, it is not at all clear what we stand to gain by using the cake pattern at all. At this point, we have to ask ourselves, did we lose anything when transitioning from the cake pattern to the modified cake pattern? Was this transition a true refactoring? In other words, are the two semantically equivalent? 

In fact, we have broken semantic equivalence in the first step, where we introduce constructors with dependencies as parameters. In the cake pattern, the dependency is resolved dynamically, at the point when it is dereferenced. Adding the constructor argument modifies this behavior so that the dependency is resolved when the service instance is constructed. This means that the cake pattern allows for mutually dependent services, while constructor injection does not. Let's consider an example to see this in action. Here is a cake pattern component with two mutually dependent services. I've thrown in some printlns so we can see at what point the services are initialized:

Now we define our TopComponent to include these two sub-components, and create a main application class that exercises the TopComponent:


The output of running Application is something like this:

hi from AService constructor, b is null
hi from BService constructor, a is AService
hi from AService greet, b is BService
hi from BService greet, a is AService

As you can see, the mutual dependencies work fine after our TopComponent is fully constructed.

Now I cannot emphasize enough that allowing for mutual dependencies is a rather dubious claim to fame. It is a basic principle of software engineering that we decouple the different parts of the software system as much as possible. And one of the basic techniques for decoupling is to restructure your code to remove mutual dependencies. We do this because two mutually dependent units of code cannot be understood in separate contexts. With a one-directional dependency, you can analyze the unit without the dependency in isolation. Once we have isolated the first unit, we can come up with an abstraction for what it is doing. Then, when analyzing the second unit, we can consider interactions with the dependency in terms of the abstraction.

Personally, I would strongly recommend against mutually dependent services. That's not to say that I haven't done it with the cake pattern! When I have allowed myself to do this, it has always come along with an idea how to fix it, and a big fat TODO comment to indicate the problem. So essentially, what I would be giving up with constructor injection is the ability to take a specific design shortcut for the sake of rapid development.

It's also important to consider that, with the cake pattern, mutual dependency is the default position for our services to take. There is nothing special you have to do to create a mutual dependency, and these things may well end up happening accidentally. It would probably be obvious to the programmer if they created a mutual dependency between two services, as in the above example. But mutual dependencies can arise in larger cycles, such as "A depends on B depends on C depends on A", where it could be quite easy for a programmer to overlook the fact that they just created a cycle.

Mutually Dependent Services in Constructor Injection

Even though I recommend against using mutually dependent services, it's really not a big deal to do this using constructor injection, and as such, really not an advantage of the cake pattern over constructor injection. All it takes is one very small modification to get there. Let's take the mutual dependency cake pattern example from the previous section, and convert it to constructor injection:


The output of running this code is as follows:

hi from AService constructor, b is null
hi from BService constructor, a is AService
hi from AService greet, b is null
hi from BService greet, a is AService

As you can see, things didn't work out the way we intended. The AService ended up with a null for its dependency, and that can't be good. But we can fix this trivially by changing the constructor arguments to call-by-name. To do this, we simply introduce the => operator into the argument type, like so:


Now our output will match the cake pattern example. AService is initialized first, and has a null BService during initialization. But in method AService.greet, after TopComponent.bService has been initialized, we get our fully resolved mutual dependency.

Other Dependency Injection Tricks

Now in my earlier work on the cake pattern, I've demonstrated a lot of useful tricks that made using the cake pattern much more expressive and powerful than an old-fashioned dependency injection framework such as Spring. None of these tricks are actually specific to the cake pattern, and can be done with constructor injection as well. I'll demonstrate this with one example that I am particularly fond of: hierarchical dependencies. Let's recall a UML diagram we presented early on in this essay:



There are a couple of higher level constraints in this diagram that are not reflected in our dependency injection examples so far. The one of interest to us is the dependency between the service layer and the repository layer. A Serv class can depend on a Repo class, but a Repo should never depend on a Serv. But there is nothing in our examples so far, nor anything provided by a framework like Spring, to enforce such a constraint. In past work, we have seen that we can enforce this using the cake pattern by introducing intermediary components for the service and repository layers. We can do the same thing with constructor injection, and it looks like this:


The self-type on ServComponent allows the services to access the repositories, but the converse is not true, so a reference to, say, authServ in the RepoComponent would cause a compiler error. So we have used constructor injection to enforce our high-level design constraint at compile time.

Martin Odersky has also presented an interesting cake-pattern technique used by the Scala compiler, where you have multiple views on the same cake: a course-grained view, and a more refined view. The same injection points are provided by both versions of the cake, but in one case, they are presented with a more limited API, and in the other case, the limited API is extended into a wider API. Again, you can use a design like this perfectly well with constructor injection instead! (You can see this in Martin Odersky's Scala Days 2012 Keynote. The discussion on the cake pattern starts at about the 8 minute mark.)

Conclusions

The main conclusion to be reached here is that I can no longer recommend using the cake pattern on any new projects. Use constructor injection instead. It will be much easier for your peers to read and understand. And it should end up being easier to maintain as well.

Personally, among other things, this means my congeal project is now officially dead. I'm still interested in writing the @api and @impl macro annotations I've discussed in conjunction with that project, but other than those, I don't see the point. It took me a little while to accept that I wouldn't be using the cake pattern any more, since I've written quite a bit on the subject in this blog. But thankfully, all of the cool tricks I learned using the cake pattern are directly transferable to the constructor injection approach.

No comments:

Post a Comment