This activator explains what Dependency Injection is and shows how to do DI in a Play application, with the help from a small library, MacWire. But without any additional frameworks! You will see how to divide the wiring of your classes into modules using traits, as well as how to use Scala Macros to remove some boilerplate code.
The bundle includes a small bootstrap script that
can start Activator. To start Lightbend Activator's UI:
In your File Explorer, navigate into the directory that the template was extracted to, right-click on the file named "activator.bat", then select "Open", and if prompted with a warning, click to continue:
Or from a command line:
C:\Users\typesafe\macwire-activator> activator ui
This will start Lightbend Activator and open this template in your browser.
Option 3: Create a macwire-activator project from the command line
If you have Lightbend Activator, use its command line mode
to create a new project from this template.
Type activator new PROJECTNAME macwire-activator on the command line.
We've included the text of this template's tutorial below,
but it may work better if you view it inside Activator
on your computer. Activator tutorials are often designed
to be interactive.
Preview the tutorial
You've just created the Macwire + Play Framework example application! Let's explore what's inside.
The main functionality of the application is to greet the user using a random greeting from an in-memory database. New greetings can be added by the user using a simple web form.
The task is quite simple and the code is probably a bit too complicated for such a simple functionality, but it should serve demo purposes well.
There are three main parts: the database module, the greetings module and the frontend part, consisting of controllers and the html pages. We'll explore each module in detail in the following sections, focusing on Dependency Injection, using traits as modules, MacWire and integration with the Play Framework.
To see how the application works, just visit http://localhost:9000 once the application has been compiled and the server started. Check Run to see the server status.
The database package
The main trait in the database package is Database. It defines a very simple interface for storing String key-pair values. It has a single implementation, InMemoryDatabase, which uses a ConcurrentHashMap for the actual storage.
Note that the InMemoryDatabase has a dependency: an AuditLogger. This implementation of the audit simply prints out information about each operation. A real-world implementation could, for example, use a RDBMS to implement the database, and write the audit to a file, including the current user's username with each entry.
The fact that InMemoryDatabase is dependent on AuditLogger is expressed using a constructor parameter. And this is the main idea behind Dependency Injection: instead of creating a specific implementation of an AuditLogger inside the InMemoryDatabase class using new, we only specify that we want "some" implementation. This decouples the two classes, as we can now use anyAuditLogger implementation, without the need to change the code of the InMemoryDatabase class.
The removal of creational concerns from the code leads to less code pollution and increased testability. Very often usage of Dependency Injection is accompanied by a framework, such as Guice, Spring or Subcut. However, a framework is not required, and as this tutorial demonstrates, Scala language features can be used instead. Also, the mentioned frameworks most often rely on run-time reflection and hence provide less type safety. That said, they offer some advanced features which are not possible to achieve only with Scala/MacWire. For a more detailed comparison, please refer to the blogs mentioned at the end of the tutorial.
The database module
The database package also contains a DatabaseModule trait. In the trait we define how to create instances of the objects from this package.
In our case, and as is most common, creating the object graph is just calling new with the right parameters. Here we additionally constrain the type of the database value to be Database, so that clients do not see the underlying more specific implementation type.
We use lazy vals, so that we don't have to worry about initialization order. When using vals objects have to be defined prior to usage. This can lead to weird NullPointerExceptions, hence if possible just use lazy vals or defs.
Trait-modules are optional
In smaller applications we could define the wiring (how the object graph is constructed) in one trait/object for all packages, however as the codebase size grows, it can become hard to manage, hence splitting into several "modules" may be a good idea. The cake pattern takes this idea even further, putting also class definitions into the traits.
Note that creating such traits is entirely optional and is not a part of the "core" Dependency Injection pattern. It is simply a way of using Scala's traits to make the code more manageable, and the pattern easier to use.
The code is fairly simple so hopefully it is easy to understand what it does.
The greetings module
The GreetingsModule differs in two important ways from the DatabaseModule. Firstly, this module itself depends on the DatabaseModule, as we will need to access the database instance. Thanks to the way Scala traits work we can express module-level dependencies and combine several modules using trait composition.
The second difference is in the way the object instances are defined: using the wire macro. A macro is a piece of Scala code which is executed at compile time, generating Scala code. The code is then type-checked and further compiled using standard rules. The wire macro is part of the MacWire library.
The wire macro will try to generate code to create a new instance of the given class, using as parameters values found in the current scope. For example,
will expand to
new RandomGreetingsService(database, randomItemChooser)
Note that this is all done at compile-time - there's no run-time component here! At run-time, all the JVM will see is
new invocation. And we get compile-time checking that all dependencies of a class are satisfied!
Wiring using the macro can be useful when classes have several dependencies and enumerating all construction parameters would be tedious. Moreover the macro can be used only for some objects, others may be created by hand or using more complex custom code.
There are two controllers. Unlike normal Play controllers, these are not objects, but classes, with some dependencies declared in constructors. Apart from that, the controllers are quite regular.
Integrating the controllers
The controllers are wired using the approach we've seen before: see the Application object. The object depends on (extends) the two modules we defined earlier, hence when wiring the controllers, the necessary parameters are available.
The last missing piece in the puzzle is how to tell Play how to obtain instances of the controllers? Firstly, we have to reference controllers in the conf/routes file with the @ prefix, e.g.:
This will cause Play to delegate looking up instances of controllers to a special class.
That special class is Global. The method that is called by Play is getControllerInstance, passing a Class corresponding to the controller that needs to be looked up. Hence we need a map translating classes to instances.
MacWire contains a utility macro to generate such a map: valsByClass(someObject). The macro will generate (again at compile-time) a map of all the vals in the given object, keyed by their classes. Finally, we are using the InstanceLookup helper class from MacWire, to be able to lookup instances taking into account super-classes and traits (e.g., our database instance will be keyed in the map generated by the macro by the InMemoryDatabase class, but we could want to look it up using the trait it implements, Database).
A very important aspect of our application is testing. One of the main benefits of Dependency Injection is the ease with which you can test components in isolation, passing in alternative implementations of any of the class dependencies.
As an example, take a look at GreetingsSaverSpec. Here we are using a mock Database, using a great mocking framework Mockito. Instead of providing a real database implementation, we only provide a mock instance, which records what calls where made to it. We can then verify that the desired interactions happened.
Using the trait-as-modules approach, it is also possible to integration-test whole packages, by instantiating the module trait. Alternative implementations can be then provided by overriding the appropriate vals.
And that's it! We now have a simple Play application, which uses Dependency Injection for wiring the services and controllers.
The whole integration layer doesn't use reflection, only type-safe, compile-time code generation. And we're only using plain, old Scala traits and classes, without the help of any framework.