There are a number of ways to do dependency injection in Scala without adding a framework. The cake pattern is probably the most popular approach and is used in the Scala compiler itself. Using implicit parameters is less popular and is used in the Scala concurrency libraries.
An approach that doesn’t get as much mention in the blogosphere but gets much love from those who have tried it is dependency injection via the Reader Monad.
To illustrate the differences between the Reader Monad approach and the more
popular approaches, let’s look at examples of using each to solve the same
dependency problem. These examples will all be injecting a dependency for
getting User objects from a repository:
1 2 3 4 | |
The Cake Pattern
In the cake pattern we represent the dependency as a component trait. We put the thing that is depended on into the trait along with an abstract method to return an instance.
1 2 3 4 5 6 7 8 9 | |
Then we declare the dependency in abstract classes using self-type declarations.
1 2 3 4 5 6 7 8 9 10 11 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
We extend the component trait to provide concrete implementations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Finally we can create an instance of our class with the dependency by mixing in a concrete implementation.
1 2 3 | |
For testing we can create a test instance of our class and mix in a mock implementation.
1 2 3 4 5 6 | |
Implicits
The cake pattern works really well but tends to require a lot of boilerplate. Another approach is to add implicit parameters to the methods that require the dependency.
1 2 3 4 5 6 7 8 9 10 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
This is a lot less code and has the advantage of allowing us to substitute a
different UserRepository at any point either by defining our own implicit
value or by passing it as an explicit argument.
A downside of this approach is that it clutters up the method signatures. Every
method that depends on a UserRepository (userEmail and userInfo in this
example) has to declare that implicit parameter.
The Reader Monad
In Scala, a unary function (a function with one parameter) is an object of type
Function1. For example, we can define a function triple that takes an Int
and triples it:
1 2 3 | |
The type of triple is Int => Int, which is just a fancy way of saying
Function1[Int, Int].
Function1 lets us create new functions from existing ones using andThen:
1 2 3 | |
The andThen method combines two unary functions into a third function that
applies the first function, then applies the second function to the result.
The parameter type of the second function has to match the result type of the
first function, but the result type of the second function can be anything:
1 2 3 | |
The type of f here is Int => String. Using andThen we can change the
result type as much as we want but the parameter type is always the same as in
the initial function.
The Reader Monad is a monad defined for unary functions, using andThen as the
map operation. A Reader, then, is just a Function1. We can wrap the
function in a scalaz.Reader to get the map and flatMap methods:
1 2 3 4 5 6 7 8 9 | |
The map and flatMap methods let us use for comprehensions to define new
Readers:
1 2 3 | |
If the above example looks strange, remember that it’s just a fancy way of writing:
1 2 3 | |
Dependency Injection with the Reader Monad
To use the Reader Monad for dependency injection we just need to define
functions with a UserRepository parameter. We can wrap each of these
functions in a scalaz.Reader to get the full monady goodness.
We define a “primitive” Reader for each operation defined in the
UserRepository trait:
1 2 3 4 5 6 7 8 9 10 11 | |
Note that these primitives return Reader[UserRepository, User] and not
User. It’s a decorated UserRepository => User that you can use with for
comprehensions. It’s a function that will eventually return a User when
given a UserRepository. The actual injection of the dependency is deferred.
We can now define all the other operations (as Readers) in terms of the primitive Readers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Because these methods use map and flatMap on the primitives (for
comprehensions use map and flatMap), the methods return new higher-level
Readers. The userEmail method in this example returns
Reader[UserRepository, String]. All of the higher-level methods dealing with
users return higher-level Readers built directly or indirectly from the
primitives.
Unlike in the implicits example, we don’t have UserRepository anywhere in
the signatures of userEmail and userInfo. We don’t have to mention
UserRepository anywhere other than our primitives. If we gain additional
dependencies we can encapsulate them all in a single Config object and we
only have to change the primitives.
For example, say we needed to add a mail service:
1 2 3 4 | |
We would only have to change our primitives to take Config instead of
UserRepository:
1 2 3 4 5 6 7 8 9 10 11 | |
Our UserInfo object doesn’t need to change. This may seem like a small win in
this example but it’s a huge win in a large application that has many times
more higher-level Readers than primitives.
Injecting the dependency
So where does the actual UserRepository get injected? All of these methods
return Readers that can get User-related stuff out of a UserRepository. The
actual dependency injection keeps getting deferred up to higher layers.
At some point we need to apply one of these Readers to a concrete
UserRepository to actually get the stuff. Normally this would be at the outer
edges of our application. In the case of a web application, this would be the
controller actions.
Assuming we have a concrete implementation of UserRepository as an object
called UserRepositoryImpl, we might define a controller like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
The object Application uses the default concrete implementation, and we can
instantiate a test version using the class Application with a mock repository
for testing. In this example we’ve also defined a convenience method run that
injects the UserRepository into a Reader and converts the result to JSON.
But I can’t choose!
There’s no rule that says we have to use just one dependency injection strategy. Why not use the Reader Monad throughout our application’s core, and the cake pattern at the outer edge?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
This way we get the benefit of the cake pattern but we only have to apply it to our controllers. The Reader Monad lets us push the injection out to the edges of our application where it belongs.