Sometimes science isn't exact. There are multiple ways to arrive at an answer depending on the situation and goals. For instance, we could calculate the free energy of a molecule with either quantum mechanics or molecular mechanics. Within molecular mechanics there are multiple popular methods, such as the MMFF force field. Inside an application that contains multiple methods for accomplishing a task, we need a way to select among options in a consistent way. Enter: dependency injection.
In software, dependency injection is a way to assemble parts of the application that depend on each other. It is a way to provide control over which specific versions of the parts get put together. The "user" of a software service doesn't create a new instance of the service. It instead asks for an implementation to be given to it. This is called "injecting" a service. This conceptual switch from "creating" to "asking" for an implementation is why dependency injection is part of what is known as Inversion of Control.
At Collective Scientific our programming language of choice is Java. The language has a specification called Context and Dependency Injection (CDI). We use the reference implementation of CDI called Weld to handle our dependency injection.
For the Service Oriented Architecture portions of our application, Weld provides the wiring between services. In many cases we have multiple implementations of a service that exist at a time. For instance, we have different ways to minimize the energy of a molecule. Some implementations can use a molecular mechanics approach with MMFF94, some use energy gradients, while others can change a protein with molecular dynamics. These each adhere to a single API contract but take very different approaches to reach an answer. The parts of our system that require energy minimization simply inject an energy minimization service. We define which of the implementations is the default. The other implementations are available if a specific need arises. We can switch the default implementation without changing each and every place that uses the service.
We use our own lightweight standalone Java application containers and the weld-se distribution. Weld is specifically initialized as part of our container's startup procedure.
We use Mockito and the InjectMocks annotation to assemble mocked implementations during Junit unit testing. This is useful for isolating tests so we're only validating the class under test and not the specifics of dependent libraries.
Using CDI and Weld mean we have an extra framework to manage and it sometimes gets in the way. When running a standalone Java application, the Weld container has to be initialized or none of the auto-wiring will happen. There is also extra work in defining the default implementation of a service. In general, these issues don't get caught until runtime rather than earlier at compile time. Simply creating new instances would be easier but we'd lose out on other functionality with contexts.
Context is Key
The other important part of CDI (the "C" part) is context. This means the injected service comes with some extra information about how pieces should be wired together. At its heart, context is about controlling the number of instances that are created. This is a scope. A scope could indicate only one instance should exist in a running application, or that a new instance should be created each time something asks for it. Our MMFF94 energy calculation service has a version that is stateless and one with cached values. We use scopes to indicate the stateless version is safe to reuse but the cached version should be created for each request.
Contexts also mean we no longer have to create singleton logic since we can annotate a service with ApplicationScoped. Let the Weld framework sort out the details. We don't have to create more tests for validating double checked locking code. The Weld container can also pool instances to save on extra instantiations. When working with a web container, there are many other useful scopes too.
Dependency injection allows us to wire together an application, manage multiple implementations, and handle how instances are created. We follow the Java CDI specification by using a weld-se distribution of Weld. The extra setup required for Weld is outweighed by the benefits realized through instance management and testing.
Also published on Medium