I was wondering about the difference between using functions and Cake pattern for DI in Scala. I came up with the following understanding(s), I would like to know if this understanding is correct.
Let's imagine a dependency graph.
1) If we use functions as building blocks then the graph consists of functions as nodes and parameters as edges.
2) If we use traits as building blocks (as in Cake) then the graph consists of traits as nodes and abstract members as edges.
So what is the purpose of Cake pattern ? Why is 2 better than 1 ? It is course graining. Graph-1 can be simplified by grouping functions into traits and then we have a smaller, more understandable Graph-2. The grouping/clustering of related concepts is a form of compression and creates understanding (we need to hold less things in our head to get an understanding).
Here is a different comparison (between Cake vs package system):
Cake is similar to grouping related functions into packages but it goes beyond that because using name-spaces (packages/objects) causes dependencies to be hard-wired, Cake is replacing packages/objects with traits and import
s with self type annotations/abstract members. The difference between packages and Cake pattern is that the actual implementation of a dependency can change using Cake while it CANNOT change when using packages.
I don't know if these analogies do make sense or not, if not please correct me, if yes, please reassure me. I am still trying to wrap my head around the Cake pattern and how to relate it to concepts that I already understand (functions, packages).
Dependency injection (DI) is commonly done with getters/setters (which is what I assume you mean by functions) and/or constructor params. The getter/setter approach may look something like this:
I don't really like the getter/setter approach. There is no guarantee that the dependency is ever injected. If you use certain DI frameworks, you can mandate something is injected, but then your DI is no longer agnostic to your framework. Now, if you use the constructor approach, the dependency must be provided whenever we instantiate (regardless of framework):
Now, how does the Cake Pattern fit in? First, let's adapt our example to the Cake Pattern:
Let's talk about
logger: Logger =>
. That's a self-type, and it simply brings the members ofLogger
into scope without having to extendLogger
.NeedsALogger
is not aLogger
, so we don't want to extend it. However,NeedsALogger
requires aLogger
, and that's what we accomplish with the self-type. We mandate that aLogger
must be provided when we create aNeedsALogger
. The usage would look like this:As you can see, we accomplish the same thing with either approach. For a lot of DI, the constructor approach will suffice, so you can just pick based on your preference. I personally like self-types and the Cake Pattern, but I see a lot of people avoiding it too.
To keep reading about the Cake Pattern specifically, check this out. It's a good next step if you'd like to know more.