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 new
s 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