Adam Warski

4 Apr 2013

MacWire 0.1: Framework-less Dependency Injection with Scala Macros

dependency injection
macwire
scala

Using Dependency Injection is almost a standard when developing software. However, in many cases it may seem that using the pattern implicates using a DI container/framework. But is a framework really needed?

To implement DI all you really need is to remove the news from your code, and move the dependencies to the constructor. There must be of course some place where the objects are created; for that you need a top-level (“main” class), where all the wiring is done. That’s where DI containers really help: they remove the tedious task of passing the right parameters to the constructors. Usually that’s done at run-time using reflection.

MacWire takes a different approach. Basing on declarations specifying which classes should be instantiated, it generates the code needed to create a new class instance, with the correct parameters taken from the enclosing type. This is done at compile-time using a Scala Macro. The code is then type-checked by the Scala compiler, so the whole process is type-safe, and if a dependency is missing, you’ll know that immediately (unlike with traditional DI containers).

For example, given:

class DatabaseAccess()
class SecurityFilter()
class UserFinder(databaseAccess: DatabaseAccess, securityFilter: SecurityFilter)
class UserStatusReader(userFinder: UserFinder)

trait UserModule {
    import com.softwaremill.macwire.MacwireMacros._

    lazy val theDatabaseAccess   = wire[DatabaseAccess]
    lazy val theSecurityFilter   = wire[SecurityFilter]
    lazy val theUserFinder       = wire[UserFinder]
    lazy val theUserStatusReader = wire[UserStatusReader]
}

The generated code will be:

trait UserModule {
    lazy val theDatabaseAccess   = new DatabaseAccess()
    lazy val theSecurityFilter   = new SecurityFilter()
    lazy val theUserFinder       = new UserFinder(theDatabaseAccess, theSecurityFilter)
    lazy val theUserStatusReader = new UserStatusReader(theUserFinder)
}

The classes that should be wired should be contained in a Scala trait, class or object (the container forms a “module”). MacWire looks up values from the enclosing type (trait/class/object), and from any super-traits/classes. Hence it is possible to combine several modules using inheritance.

Currently two scopes are supported; the dependency can be a singleton (declared as a val/lazy val) or a new instance can be created for each usage (declared as a def).

Note that this approach is very flexible; all that we are dealing with here is regular Scala code, so if a class needs to be created in some special way, there’s nothing stopping us from simply writing it down as code. Also, wire[T] can be nested inside a method’s body, and it will be expanded to new instance creation as well.

For integration testing a module, if for some classes we’d like to use a mock, a simple override suffices, e.g.:

trait UserModuleForTests extends UserModule {
    override lazy val theDatabaseAccess = mockDatabaseAccess
    override lazy val theSecurityFilter = mockSecurityFilter
}

The project is a follow up of my earlier blog post. There is also a similar project for Java, which uses annotation processors: Dagger.

MacWire is available in Maven Central. To use, simply add this line to your SBT build:

libraryDependencies += "com.softwaremill.macwire" %% "core" % "0.1"

The code is on GitHub, licensed under the Apache2 license, so feel free to use, fork & explore. Take a look at the README which contains some more information.

Future plans include support for factories and by-name parameter lookup, support for configuration values and more scopes, like request or session scopes, which are very useful for web projects.

Adam

comments powered by Disqus

Any questions?

Can’t find the answer you’re looking for?