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:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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:
1 2 3 4 5 6 | 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.:
1 2 3 4 | 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:
1 | 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