måndag 7 maj 2012

Using implicit parameters for Dependency Injection in Scala

A few days ago I made a post about some issues I have with the cake pattern for dependency injection. I want to illustrate a means of doing DI using scala's implicit parameters which retain the benefits of the cake pattern, but is more consice. The main benefit I see with the cake pattern is it's type safety. If we do something wrong the compiler will tell us, so we don't have to wait until we actually deploy to find out if our configuration is correct.

In order to understand how to do DI with implicit parameters we need to understand how implicit parametrs are resolved in Scala. In essence if we define a method with an implicit parameter:
When the compiler finds a call to a method with an implicit parameter it searches for a implicit val/var/def/object that provide an instance of a matching type. In this case I defined method that take a string as an implicit parameter. When I tried to call it without having an implicit value of type String in scope I got an error. Then specified an implicit val, and calling the method now prints the value of that val. We can however override this behaviour by providing the method with an argument explicitly such as:
For more information on how implicit parameters work and are resolved in Scala there are some excellent short blog posts on Daily Scala about it:
  1. http://daily-scala.blogspot.se/2010/04/companion-object-implicits.html
  2. http://daily-scala.blogspot.se/2010/04/implicit-parameters.html
  3. http://daily-scala.blogspot.se/2010/04/implicit-parameter-resolution.html

From the last post the resolution rules for implicits is summarized as:
There are very strict rules for which implicit value is to be applied to a implicit parameter. A simple way to think about it is that the "closest" definition will be used. Local scope, enclosing class, parent class, companion object of the desired type.
With this knowledge in our minds let's explore how we can use implicit parameters for dependency injection. For comparison I will show the same service/dao setup as in my previous post.
Since an implicit parameter can be provided explicitly it is trivial to mock the FooDao when testing the FooServiceImpl. I think this is perhaps the main advantage of using DI. For comparison, using this means of dependency injection the code is succint and not poluted with alot of boilerplate (12 LoC compared to 25 LoC using the cake pattern). One issue still remains. How do we ensure that FooDao is injected into the FooServiceImpl?
The main difference between the two approaches above is that if we extend Conf in our class in order to get the dependencies we need then we will have several Conf instances around and all of them will contain a fooService and a fooDao. So I think the former approach is preferable for this reason.

Another thing to notice is that in the Conf trait I have not explicitly provided FooServiceImpl with a FooDao when it is instantiated. Instead the compiler will resolve it for us using the locally defined fooDao val. Using this pattern we avoid having to spread a bunch of implicit val/def/var/object in our code. We have one central place where our configuration of the app is performed. If we make a mistake, such as defining two implicits with the same type in our conf so that the compiler don't know which to pick, we get a compiler error. Sweet!

But, wait. What if we have a circular dependency? First, try to avoid circular dependencies. Second, we could resolve it by passing a factory function to the class:
Then in our configuration do something like:
Jonas Bonér has already described this pattern in his blog post about different approaches to dependency injection. However, he states a thing I quite do not get:
This approach is simple and straight-forward. But I don’t really like that the actual wiring (importing the implicit declarations) is scattered and tangled with the application code.
This is obviously true if we choose to import the implicit definitions everywhere that we instantiate the classes. However, by using Conf as a factory for our dependencies we avoid having to import the implicits everywhere when we want a new instance of a certain "top level" class.

Inga kommentarer:

Skicka en kommentar