How MacWire works
MacWire uses scala macros to instantiate new objects, using values in the enclosing types for constructor parameters.
Consider 2 services, X and Y, and three dependencies, A, B and C:
1
2
3
4
5
class A
class B
class C
class X(a: A, b: B)
class Y(c: C)
Without MacWire, we can instantiate the services like this:
1
2
3
4
5
lazy val a = new A
lazy val b = new B
lazy val c = new C
lazy val x = new X(a, b)
lazy val y = new Y(c)
Since there is exactly one value of type A and exactly one value of type B, the compiler can understand that the only possible way of creating the service X is by passing these values to the X constructor. Similarly for Y.
With MacWire, we can write the code above like this:
1
2
3
4
5
lazy val a = new A
lazy val b = new B
lazy val c = new C
lazy val x = wire[X] // code is replaced during compilation from `wire[X]` to `new X(a, b)`
lazy val y = wire[Y]
The wire
macro simply looks for the types of the parameters declared in X constructor, and passes a value as an argument as long as there is exactly one in scope.
Otherwise, it fails the compilation.
Another way of writing the code above is simply:
1
2
3
4
5
lazy val a = wire[A]
lazy val b = wire[B]
lazy val c = wire[C]
lazy val x = wire[X]
lazy val y = wire[Y]
Since the constructors for A, B and C have no arguments, the wire macro will just instantiate them by replacing the macro with new A
, new B
and new C
.
The code above would stay exactly the same if we removed dependency A
from service X or added dependency B
to service Y (and the result of the macro-expansions would obviously change to new X(b)
and new Y(b, c)
).
This means that we can create the dependencies once, and inject them just by demanding them in the constructor, which is similar to Guice, but with added compile-time safety.
We could have added MacWire right after adding compile-time dependency injection to the project, but I wanted you, the reader, to practice manual depenency injection (using just
new
) so it would help you understand how it works.
Add MacWire to our build
Lets add MacWire as a dependency in build.sbt:
1
2
3
4
libraryDependencies ++= Seq(
// more dependencies here
"com.softwaremill.macwire" %% "macros" % "2.3.3" % Provided
)
Notice that the scope for this dependency is provided.
Dependencies in the compile
scope exist on the runtime, test and compile classpaths.
Dependencies in the test
scope exist on the test and compile classpaths, but not on the runtime classpath.
Dependencies in the provided
scope exist on the compile classpath, and are excluded when creating a package distribution since they are expected to be provided.
Since wire
is a macro, it is already expanded during compilation, so later this dependency is no longer needed anyway. Scoping it as provided
means that its jars won’t even be on the classpath since we never need to load them anyway. Usually, most of the dependencies in your build, including Guice, will be in the compile scope.
Import changes in IntelliJ so it will download the dependencies for you and help you with code completion (it will also reload the sbt shell behind the scenes).
Add import com.softwaremill.macwire.wire
to the components classes (AppComponents
, DevComponents
, ProdComponents
and TestComponents
).
Use MacWire
In the components classes, change new Service(dependencies)
to wire[Service]
.
For example, the new code for instantiating the controllers and the router in AppComponents will be:
1
2
3
lazy val homeController: HomeController = wire[HomeController]
lazy val dishController: DishController = wire[DishController]
override def router: Router = wire[Routes]
And in DevComponents, the code for wiring the depenencies for the in-memory dish library will be:
1
override val dishLibrary: DishLibrary = wire[DishLibraryInMemory]