Saturday, July 12, 2014

Roll-your-own Dependency Injection in Scala

This post assumes you know what DI is and why it's a good thing(tm). If you're not on board with that, here's a StackOverflow answer that might help.

Since Scala runs on the JVM there are countless examples of dependency injection solutions available to any Scala project. There are frameworks like Spring and Guice that will provide you a fully configurable and highly extensible solutions, and there are even some native Scala solutions like Scaldi. Many Scala devs have taken a crack at super simple DI solutions, like Jason Arhart's Reader Monad and Michael Pollmeier's Service pattern. It is into this ring that I would like to throw my hat.



Services and Locators


The pattern that I came up with ended up being a small variation on the services idea with some chrome around it to make it easer to register and consume global services (note that this solution only applies to globally addressable services, you would want to drop the registry and locator pattern for scoped services). It's important to note that the registry below is considered an anti-pattern by some, and in truth it probably is for APIs that will be consumed outside your project. I just happen to like the convenience of it in my smaller side projects.

The gist of it is that we have a hashmap[class manifest -> service impl] registry that the companion objects of the service interface may act as a convenient locator for through their apply method. At app or test bootup we inform the registry of available services, then the companion object locators are used to apply them as needed at runtime. Code speaks louder than words so here we go..



The Code


We start with the base service interface which only requires that the services we implement in our project declare explicitly what their class manifest is so that we have a convenient unique handle to each service. This could be a string or class path or something silly like that, but I like the manifest better because it will never get out of sync with the interface.
/**
 * Injectable Service interface
 */
trait Service {

  /**
   * Explicit manifest declaration of the Service Interface,
   *  allows Service registration to omit a type parameter
   *  NB: this should always be a final def in the Service Interface
   */
  def serviceManifest: Manifest[_]

}
The companion objects of the service interfaces that we create are the convenience locators for our services. By defining them with the service manifest as an implicit parameter they can provide the registry with the appropriate request without having to figure out what interface the caller is asking for.
import scala.reflect.Manifest

/**
 * Base class for all Service companion objects
 *  Makes them into nifty service locators
 */
abstract class ServiceCompanion[T <: Service](implicit m: Manifest[T]) {

  def apply() = Services.apply[T]

}
A centralized boostrap isn't strictly necessary but it's a convenient place for us to define our standard services.
/**
 * Injects configuration into Services and boots them up. 
 *  If it's configurable, it belongs here.
 */
object ServicesBootstrap {

  /**
   * Trigger and status indicator for executing bootstrap startup behavior
   *  (i.e. registering services)
   */
  lazy val registered = {

    Services.register(new ExampleServiceImpl(
      "configurable value 1",
      "2",
      "3"
    ))

    true
  }

}
Finally we get to our first service interface. This example service promises to doSomeStuff and identifies itself to the registry with its own class manifest. Its companion object similarly identifies itself as belonging to this service with the manifest, and now has an apply() method that will retrieve whatever instance of it is defined in the registry.
trait ExampleService extends Service {

  final def serviceManifest = manifest[ExampleService]

  def doSomeStuff(): String

}
object ExampleService extends ServiceCompanion[ExampleService]
An implementation of our service interface takes in some configuration parameters, which presumably would come from the conf file or some other config mechanism. In this case we just concatenate the values we're provided together, comma-separated.
class ExampleServiceImpl(
  conf1: String,
  conf2: String,
  conf3: String) extends ExampleService {

  def doSomeStuff(): String = {
    s"$conf1, $conf2, $conf3"
  }
}
Now we have enough infrastructure to use our service in a real app. For simplicity's sake we just execute our bootstrap and call our service, asserting that it returns our configured values.
assert(ServicesBootstrap.registered)
assert(ExampleService.doSomeStuff() == "configurable value 1, 2, 3")
The juicy bit, and most of the point of DI, is mocking services out to exclude their logic from tests that have nothing to do with them. Here we are setting up a mock service that will return whatever simple value is passed into it at creation time, so that we can control for usages of ExampleService in other business logic. (note that normally we wouldn't go about this by creating an actual mock class, we'd use something like Mockito or ScalaMock to generate it for us, that's just outside the scope of this post.. in fact I'll do one later on that).
class ExampleServiceMock(result: String) extends ExampleService {
  def doSomeStuff(): String = result
}
object ExampleServiceMock {
  val doSomeStuffResult = "wahoo!"
}
Speaking of other business logic, here's a snippet of some that does who-knows-what and also happens to call ExampleService.doSomeStuff, eventually returning it as the result of its operation.
object SomeBusinessLogic {
  def operation(): String = {
    // do some business logic 
    val stuff = ExampleService().doSomeStuff()
    // do some more business logic
    stuff
  }
}
To eliminate the chance of something going wrong with ExampleServiceImpl during our test (i.e. to make our test as narrow and specific as possible), we register an instance of ExampleServiceMock that will return exactly the result we expect, regardless of what configuration or other changes are made to ExampleServiceImpl.
class SomeBusinessLogicTest extends FunSuite {

  val exampleService = new ExampleServiceMock(ExampleServiceMock.result)

  test("some business logic operation") {
    assert(SomeBusinessLogic.operation() == ExampleServiceMock.result)
  }

}
So that's pretty cool, but it has some serious drawbacks, chief among which is that we have just destroyed our ability to run tests in parallel. If some other test also registers its own instance of ExampleServiceMock we will have a potential race condition if that test and this test are executed in parallel, possibly causing flappy tests and indeterminate behavior. The net is that this is a neat and cheapo toy for quick and dirty side projects, but probably a bad thing(tm) for an real efforts, just as Mark Seemann says.