In this post we'll cover Dependency Injection (DI) basics in scala, and focus on how to perform it manually with using the cake pattern
.
All code examples are available on github.
Introduction to DI
Dependency injection encourages loose coupling. It tells you that this is wrong:
class MyRepository
class BlueService {
val a = new MyRepository()
}
and rather encourages you to instead do:
class MyRepository
class BlueService(a: MyRepository) {}
With this change we're defining explicitly which dependencies the MyService
constructor needs, and that those should be injected from outside. The rule of thumb: if you see the new
keyword inside a service, that should immediately yield a red flag in your head.
Might not seem much, but this is already a big improvement, as it greatly simplifies the way we can build tests for the MyService
class.
Let's do that. So let's start by defining our dependencies (no pun intended) in build.sbt
:
libraryDependencies ++= Seq(
"org.scalamock" %% "scalamock" % "5.2.0" % Test,
"org.scalatest" %% "scalatest" % "3.2.14" % Test
)
And here a sample layout for our mocking:
import eu.m6r.scalalightningtalks.di.Background.DI2.{BlueService, MyRepository}
import org.scalamock.scalatest.MockFactory
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.must.Matchers
class BackgroundTest extends AnyFlatSpec with Matchers with MockFactory {
it should "test something BlueService when repository behaves a certain way" in {
val mockRepository = mock[MyRepository]
// service under test
val sut = new BlueService(mockRepository)
// ...
}
}
And this way we can easily manipulate the response from MyRepository. If our original implementation was:
class MyRepository {
def findOne(): String = "one"
}
class BlueService(a: MyRepository) {
def get(): String = a.findOne()
}
.. then we could mock the response, for example, as such:
it should "test something BlueService when repository behaves a certain way" in {
val mockRepository = mock[MyRepository]
// service under test
val sut = new BlueService(mockRepository)
// mock
(mockRepository.findOne _)
.expects()
.returning("two")
.once()
sut.get() shouldBe "two"
}
However, we're not done yet; consider the following:
trait Repository
class MyRepository extends Repository
class BlueService(a: Repository) {}
By defining a general contract for the Repository
(via our trait), we are nicely adding a separation of concerns - in the future we can easily swap to a different concrete Repository
implementation (for example, instead using a given DB, using another), without having to change anything in BlueService
implementation.
If this is your first time hearing about it, you may be asking yourself: But where then do I instantiate and pass the concrete Repository instance?
In other words: Where do I instantiate and inject BlueService
dependency?
DI relies on Inversion of Control (IoC) principle, which encourages one to create the concrete service implementations outside of the actual services that use them. This is usually done in a central container, or spread out across multiple centralization containers which determine which dependency is instantiated and gets injected to which class.
Options for DI in Scala
We have several options for DI in Scala:
- using libraries from java world, such as Guice;
- using pure scala: manually injecting via a central container, with the
cake pattern
, or with thereader monad
; - using scala libraries: MacWire, SubCut, Scaldi
In this post we'll just show case the most basic form of manual DI, and then exemplify the second alternative with the cake pattern
.
Manual DI - central container
Let's start with the simplest, and arguably the most frowned upon. If we're being honest here, quite a reasonable one if you're working on a small project.
Just create a main container where your configuration(s) is/are loaded, and passed to create dependencies, and inject them in several services.
trait Repository
class MyRepository extends Repository
class BlueService(repository: Repository)
class GreenService(repository: Repository)
object {
val myRepo = new MyRepository()
val blueService = new BlueService(myRepo)
val greenService = new GreenService(myRepo)
}
The beauty about this solution is two-folded: one it is very simple - it is straight forward, anyone can understand it, there is no magic happening underneath the hood. The second is that we have compile type safety giving us compile time seat-belt for any mistakes.
This has one core drawback, which is that it is not very scalable. It requires tediously declaring all new dependencies, increasing linearly as the project grows, while making sure that the exact instantiation and injection are guaranteed at the right time and place. Eventually we might bump into one of these mistakes:
object {
val blueService = new BlueService(myRepo)
val myRepo = new MyRepository()
}
.. and be sure that this will compile, but myRepo
will be null
when referenced, since it hasn't been initialized (a case of forward reference).
Arguably not the worst thing in the world, and easily solvable by letting the compiler solve it for us with the keyword lazy
:
object {
lazy val blueService = new BlueService(myRepo)
lazy val myRepo = new MyRepository()
}
However not all is bad; in fact, manual DI does not get nearly as much love as it should. In comparison with most DI frameworks, which use reflection at run-time to dynamically infer the dependency graph, it gives us type-safety at compile time, a slight startup time improvement, and, most of all, removes the magic done behind the scenes. I can't state this enough: no framework magic to fight against (looking at you spring).
Manual DI - Cake pattern
The key building block to implement the cake pattern is to relie on trait mix-in
instead of inheritance.
The name cake
comes from the idea of multiple layers - as we mix-in multiple traits with each other.
We will dive right next in how the mix-in mumbo jumbo is executed - but just keep in mind that this is how one hooks dependencies, which forces the implementer of the traits to then inject concrete definitions of them.
Let's review some fundamentals about scala traits to make the previous paragraph make any sense.
cake pattern background: traits & diamond problem
The closest thing to a trait in the java world would be an interfaces - more concretely a java 8+ interface. Since java 8, interfaces can even have concrete method implementation using the default
keyword, and so can traits. One interface can also, like scala traits, extend more than one interface:
interface Demo1 {}
interface Demo2 {}
// this is fine
interface Demo3 extends Demo1, Demo2 {}
This might ring some alarm bells and cold sweats in you, namely regarding the diamond problem.
The problem comes when the original interfaces being extended share one or more methods with the same signature:
private interface Demo1 {
default String greet() {
return "hello";
}
}
private interface Demo2 {
default String greet() {
return "hallo";
}
}
// interface can extend multiple interfaces,
// however on conflict, one will need to override
private interface Demo3 extends Demo1, Demo2 {
@Override
default String greet() {
return Demo1.super.greet();
}
}
The way java handles this situation is by forcing the developer to explicitly implement the concrete method in conflict - the greet
method in our silly example.
Like java interfaces, one can extend a trait with another trait:
trait FileReader
trait OsFileLocker
// this is also valid syntax:
trait FileWriter extends FileReader with OsFileLocker
However, in presence of conflicting method signatures, scala will behave differently. Here are more complete illustration of the diamond problem:
trait FileOps {
def readFile: String
}
trait FileReader extends FileOps {
override def readFile: String = "loaded"
}
trait OsFileLocker extends FileOps {
override def readFile: String = "lock created & file loaded"
}
class FileSystemConsumer extends FileReader with OsFileLocker
Note: extending in Scala 3 is, in my opinion, clearer: class FileSystemOperator extends FileWriter, OsFileLocker
The scala compiler will allow such wiring and apply a simple disambiguation rule: on conflicting method signature, use the implementation of the last trait being extended, e.g. the one one furthest to the right (OsFileLocker
).
println(new FileSystemConsumer().readFile)
// will print: lock created & file loaded
To make it perfectly clear, if one swapped the order, the print message would simply be "loaded":
class FileSystemConsumer extends OsFileLocker with FileReader
println(new FileSystemConsumer().readFile)
// will print: loaded
cake pattern background: self type annotation
The second essential trick we'll be using is scala's self
type - which provides a way to mix-in another trait:
trait Greeting {
def intro: String
}
trait Greeter {
this: Greeting =>
def greet: String = s"$intro, I am Foo"
}
By specifying this: Greeting =>
we are defining that whoever extends Greeter
will need to also extend the Greeter
trait, or anything that extends it. The anything that extends it is the relevant part, and the main reason why we don't simply declare trait Greeter extends Greeting
.
If one tried to instantiate a Greeter trait, the compiler would immediately complain:
// this won't compile
val greeter = new Greeter
But if one would define a concrete implementation, for example:
trait PortugueseGreeting extends Greeting {
override def intro: String = "Olá"
}
Then the following would compile:
val greeter = new Greeter with PortugueseGreeting
println(greeter.greet)
// will print: Olá, I am Foo
The key part here to understand is the with PortugueseGreeting
- the concrete part where we're injecting the intended concrete implementation of the Greeting
contract. One would not be able to force the developer to inject a concrete implementation if one would have used inheritance to define the Greeter
trait:
// bad: using inheritance
trait Greeter with Greeting {
def greet: String = s"$intro, I am Foo"
}
Note that anything available in the Greeting
trait will also be available for the Greeter
trait - in our silly example the intro
method - when using the preferred mix-in implementation.
the cake pattern example (finally)
We are going to use an example often seen in API's code, namely where we want to specify that a service (where business logic resides) needs to be injected with a generic implementation of a repository (where we define CRUD operations with a given data store).
Let's keep this simple; our model:
case class User(id: Long, name: String)
And our generic UserRepository contract:
trait UserRepository {
def findOne(): User
}
The second step is where specific cake pattern design would kick in. One would define a component, say UserRepositoryComponent
:
trait UserRepositoryComponent {
// expose the generic contract
val userRepo: UserRepository
}
Components
have a single purpose: to expose a given contract. Given the property that we talked earlier that the exposed value - userRepo
in our example - will be available for use in the trait where any other trait where it is mixed-in.
So if we had a generic service contract:
trait UserService {
def findUser: User
}
We could wire all of these together in a UserServiceComponent
:
trait UserServiceComponent {
this: UserRepositoryComponent =>
val userService: UserService = new MyUserService
class MyUserService extends UserService {
def findUser: User = userRepo.findOne()
}
}
I hope by now the reason for the name cake
starts becoming clearer. Like a cake with several layers, our component declares a class inside of it, which, in turn, makes use of the userRepo
value thanks to the this: UserRepositoryComponent =>
mix-in.
At this point we're exactly in the same situation as we were when we couldn't instantiate a new Greeter
instance, since we still can't instantiate a UserServiceComponent
. No worries, let's implement a concrete mock Repository:
trait CassandraUserRepositoryComponent extends UserRepositoryComponent {
val userRepo = new CassandraUserRepository
class CassandraUserRepository extends UserRepository {
def findOne(): User = User(1L, "john")
}
}
Now we can glue it all together:
val userServiceComponent = new UserServiceComponent with CassandraUserRepositoryComponent
println(userServiceComponent.userService.findUser)
// will print: User(1,john)
Just like before, we're leveraging scala self-type to inject the concrete implementation of our repository only on instantiation.
Conclusion
In this post we went through the basics of dependency injection with examples in scala, and dived into a concrete way to wire your code to perform DI manually, with the cake pattern. As mentioned in the beginning, feel free to grab the code snippets available on github to play with them.
We sincerely hope that we were able to distill all building blocks of the cake pattern in simple terms that didn't require you to take a PhD in category theory.
References
- Scala
self
types - Really old, but great post about the Cake Pattern by industry heavy weight Jonas Bonér, CTO at Lightbend
- Blog post distilling all required building blocks in Scala for using the Cake pattern
- A fabulous deep dive to DI in scala from the creator himself of the
MacWire
library, Adam Warski - Scala Mock quickstart
- Scala test with Mock documentation
Top comments (0)