Stairs, Cakes and Injections

StairsCakes

Dependency Injection is a design pattern meant to increase decoupling, readability and testability by wiring together component implementations so that:

  • All their dependencies on other components’ contracts are satisfied
  • The wiring logic doesn’t belong to any component being wired but to injectors instead
  • Injectors can evaluate wiring rules at runtime
  • Multiple, different injectors can coexist in a single execution

Scala supports typesafe Dependency Injection and there are even several ways to implement it. Possibly the most popular one is based on the cake-pattern, which leverages Scala’s self type annotations; yet again I couldn’t find an explanation of it that was crystal clear to me, so I decided to build my own.

There are four distinct conceptual layers to this Scala DI implementation strategy and each  of them deserves explanation.

Layer 1: Service Defs -> Ingredients

Service definitions are just common service interfaces without anything else, for example the following couple of traits are service definitions:

trait LoginService {
  def authenticate(username: String, password: String): Session
}

trait PolicyService {
  def authorize(username: String, password: String): Boolean
}

Layer 2: Component Defs -> Cake Parts’ Recipes

Here’s the first trick: a component definition packages in a single trait one or more service definitions together with an abstract reference to each of them, so that the component’s contract is basically made of the references to the packaged services. For example:

trait LoginComponent {
  val loginService: LoginService

  trait LoginService {
    def authenticate(username: String, password: String): Option[Session]
  }
}

trait PolicyComponent {
  val policyService: PolicyService

  trait PolicyService {
    def authorize(username: String, password: String): Boolean
  }
}

Layer 3: Component Impls -> Actual Cake Parts

We’ve come to the second trick. A component implementation provides a specification of component definitions it depends on, in the form of self type annotations, so that other components’ abstract references to services can be used in its service implementations:

trait LoginComponentImpl extends LoginComponent {
  class LoginServiceImpl extends LoginService {
    def authenticate(username: String, password: String): Option[Session] =
      ... // Some impl
  }
}

trait PolicyComponentImpl extends PolicyComponent {
  // Dependency, requires this trait to be mixed in _at least_ with a LoginComponent
  self: LoginComponent =>

  class PolicyServiceImpl extends PolicyService {
    def authorize(username: String, password: String): Boolean =
      loginService.authenticate(username, password).isDefined &&
        ... // Some other impl
  }
}

Layer 4: Registries -> Putting Cakes Together

And finally the last trick: we define injector (cake) instances putting together all the inter-dependent slices into specific cakes. An injector specifies directly which implementations it wires together as well as, indirectly, which interfaces it provides.
Scala’s type-system will check for us at each cake’s instantiation time that the actual slices can stay together, by ensuring that slices’ dependencies (self type annotations) are indeed satisfied.
Of course instantiation requires that all abstract values are made concrete, so the type system will also force us to specify an implementation for each and every abstract service reference.

// This will check both dependencies and concretization
object MyFirstInjector extends PolicyComponentImpl with LoginComponentImpl {
  override val loginService = new LoginServiceImpl
  override val policyService = new PolicyServiceImpl
}

Cakes are served: we can finally write program code that will make use of whatever components wiring cake (so, registry) we prefer!

Leave a Reply