1. Overview
In our tutorial on Scala’s Self-Type Annotation, we saw how to use self-type annotations to declare dependencies among types.
Now, it’s time to go further. Let’s talk about the Cake Pattern, an idiomatic implementation of the Dependency Injection pattern in Scala.
2. Setting Up the Environment
We start by revamping the traits and classes we defined in the previous tutorial. Imagine we want to set up a framework to execute tests. We need a type that represents the execution environment:
trait TestEnvironment {
val envName: String
def readEnvironmentProperties: Map[String, String]
}
and a type that is the thing that executes the tests:
class TestExecutor { env: TestEnvironment =>
def execute(tests: List[Test]): Boolean = {
println(s"Executing test with $envName environment")
tests.forall(_.execute(readEnvironmentProperties))
}
}
To run the tests, the executor needs an instance of TestEnvironment. In Scala, we can express this constraint using a self-type annotation. The notation env: TestEnvironment => declares the dependency from the type TestEnvironment, calling it env.
Let’s see how we can use self-type annotation as the base to create a full implementation of the Dependency Injection pattern.
3. Idiomatic Dependency Injection: the Cake Pattern
Using self-types, we can build a sort of resolution mechanism of dependencies among types. It’s called the Cake pattern, a sort of dependency injection mechanism that uses only idiomatic Scala constructs.
The cake pattern represents the most important use of the self-type annotation in Scala.
The pattern defines two different aspects of dependency management. The first is how to declare a dependency. The second is how to resolve a dependency.
3.1. Dependency Declaration
In the previous examples, we mixed business logic and dependency resolution in a single type. A better approach would be to separate the business logic from the code used to manage dependencies.
First of all, let’s lift the self-type annotation away from the TestExecutor class. We need to define new scopes that enclose our types:
trait TestEnvironmentComponent {
val env: TestEnvironment
trait TestEnvironment {
val envName: String
def readEnvironmentProperties: Map[String, String]
}
}
To identify the scope, we use a trait called TestEnvironmentComponent. The TestEnvironment instance is available through the env variable declared in the scope. The trait TestEnvironmentComponent is responsible for the availability of all of the TestExecutor class instances among the application. In this case, the variable env represents a singleton since we used a val reference.
We proceed to define a scope also for the TestExecutor class. We can put the dependency declaration in this scope object:
trait TestExecutorComponent { this: TestEnvironmentComponent =>
val testExecutor: TestExecutor
class TestExecutor {
def execute(tests: List[Test]): Boolean = {
println(s"Executing test with ${env.envName} environment")
tests.forall(_.execute(env.readEnvironmentProperties))
}
}
}
The critical points of the previous code are:
- The declaration of the dependency is lifted into the TestExecutorComponent trait
- The dependency from the TestEnvironment type does not pollute the code of the TextExecutor class anymore
- We access the TestEnvironment instance through the env variable
If we need to inject more than a straightforward dependency, we can use the self-type annotation stacking multiple traits. Imagine that we want an executor that uses a logging framework:
trait LoggingComponent {
val logger: Logger
class Logger {
def log(level: String, message: String): Unit =
println(s"$level - $message")
}
}
We can mix the LoggingComponent in our executor, along with the TestEnvironmentComponent:
trait TestExecutorComponentWithLogging { this: TestEnvironmentComponent with LoggingComponent =>
val testExecutor: TestExecutor
class TestExecutor {
def execute(tests: List[Test]): Boolean = {
logger.log("INFO", s"Executing test with ${env.envName} environment")
tests.forall(_.execute(env.readEnvironmentProperties))
}
}
}
All that we’ve got to do now is to resolve the dependency. Let’s have a look at it.
3.2. Dependency Resolution
Until now, we have set up an environment that allows us to declare dependencies. It’s time to elegantly resolve them.
We need a context – or a registry – storing all the objects in the application. We can merely mix the component objects into the registry and instantiate any left abstract reference:
object Registry extends TestExecutorComponent with TestEnvironmentComponent {
override val env: Registry.TestEnvironment = new WindowsTestEnvironment
override val testExecutor: Registry.TestExecutor = new TestExecutor
}
The Registry object extends both the TestExecutorComponent trait and the TestEnvironmentComponent trait. Thus, the compiler can resolve the self-type annotations with the concrete instances.
The cake pattern allows us to declare different registry objects. For example, let’s say we need to write a unit test for the TestExecutor class. We need to mock the TestEnvironment and inject the mocked reference to the executor object.
First of all, we convert the registry object into a trait. Then, we can use it to mix into the unit test the proper classes’ instances.
For instance, to create a unit test for the type TestExecutor, we have to mock every instance of the type TestEnvironment:
trait TestRegistry
extends CakePattern.TestExecutorComponent
with CakePattern.TestEnvironmentComponent
with MockFactory {
override val env: TestEnvironment = mock[TestEnvironment]
override val testExecutor: TestExecutor = new TestExecutor
}
We mock the variable env using the mocking library ScalaMock, and then we let the compiler inject such reference in the testExecutor variable.
4. Conclusions
Starting from the concepts we saw in Self-Type Annotation in Scala, we’ve built a full framework to resolve dependencies among types, using only idiomatic constructs of the Scala language: The Cake Pattern.
As usual, the source code for this tutorial is available over on GitHub.