DEV Community

diogoaurelio
diogoaurelio

Posted on

Dependency Injection in Scala - cake pattern

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() 
}

Enter fullscreen mode Exit fullscreen mode

and rather encourages you to instead do:

class MyRepository
class BlueService(a: MyRepository) {}
Enter fullscreen mode Exit fullscreen mode

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
)

Enter fullscreen mode Exit fullscreen mode

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)
    // ...
  }

}

Enter fullscreen mode Exit fullscreen mode

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()
    }

Enter fullscreen mode Exit fullscreen mode

.. 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"

  }

Enter fullscreen mode Exit fullscreen mode

However, we're not done yet; consider the following:

trait Repository
class MyRepository extends Repository

class BlueService(a: Repository) {}

Enter fullscreen mode Exit fullscreen mode

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 the reader 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)
}

Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

.. 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()
}
Enter fullscreen mode Exit fullscreen mode

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 {}

Enter fullscreen mode Exit fullscreen mode

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();
        }
    }

Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

But if one would define a concrete implementation, for example:


  trait PortugueseGreeting extends Greeting {
    override def intro: String = "Olá"
  }
Enter fullscreen mode Exit fullscreen mode

Then the following would compile:

    val greeter = new Greeter with PortugueseGreeting
    println(greeter.greet)
    // will print: Olá, I am Foo
Enter fullscreen mode Exit fullscreen mode

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"
    }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

And our generic UserRepository contract:

    trait UserRepository {
      def findOne(): User
    }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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
    }

Enter fullscreen mode Exit fullscreen mode

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()
    }

  }

Enter fullscreen mode Exit fullscreen mode

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")
    }
  }

Enter fullscreen mode Exit fullscreen mode

Now we can glue it all together:


    val userServiceComponent = new UserServiceComponent with CassandraUserRepositoryComponent
  println(userServiceComponent.userService.findUser)
  // will print: User(1,john)

Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)