Saturday, August 25, 2012

In the tradition of modular and object oriented programming, we have long learned to design software by hierarchical decomposition - divide and conquer engineering, where each module/object has a clear function/responsibility. Complex functionality is achieved by delegating some sub-functionality to other modules/objects.

In the above example, module A achieves its functionality with the help of B, C and so one. When these functions become stateful, abstract data types or objects, "wiring" up this dependency tree to enable the access to the right instances of data at each level can become non-trivial in large projects. The dependencies can be hidden and encapsulated hierarchically such that if an application needs an "A", creating "A" in turn triggers the creation of the appropriate "B", "C", "D" and "E", hiding all the complexity of the decomposition from the user of "A".

However this static setup can pose some challenges for unit-testing. The leave-nodes can usually be quite easily unit-tested in isolation, as well as higher-level modules which don't depend on anything which creates explicit external interactions or dependencies. But if for examples, "D" is a database client and "E" a nuclear reactor controller, then "C" and "A" can't certainly be tested in such a naive manner. The solution for this dilemma is typically to introduce special testing code in either "C" or "D" and "E" to fake part of the functionality without external dependency. In complex systems and without any further support, testing often degenerates into unit-testing only for the basic low-level modules in combination with automated system or sub-system test scenarios using complex simulators to resolve dependencies on an external environment.

In languages which easily support interface inheritance and runtime polymorphism (e.g. Java, Python and to a lesser degree C++), we can easily do better for unit-testing at every level and without mixing production and testing code. However, for that we have to get away from dependency encapsulation to dependency injection.

For example, instead of having "A" create an instance of "B" and "C" as needed, they could be passed in as arguments to the constructor of "A". This then allows to unit-test "A" in isolation by injecting mock version of "B" and "C" for the test. There are a few framework, which help to automate and simplify greatly the creation of such mock objects (e.g. EasyMock or Mockito).

While dependency injection and mocking greatly simplifies testing, it makes the actual production code more complex. Instead of getting an abstract and encapsulated "A" somewhere in the code, we now need to deal with setting up the entire dependency tree of "A" each time and everywhere we need an instance o "A", making all the dependencies of "A" explicit and visible. This seems a step in the wrong direction...

An alternative to manually "wiring up" object dependency trees, there are frameworks for automating this process. The only one I am really familiar with is Guice for Java. With Guice object runtime dependencies are defined through a combination of annotations and declarative java code, which can be hierarchically decomposed, typically at package level and include definitions of lifecycles (scopes) and how interface dependencies should be satisfied by concreate implementations. At application runtime, the Guice injector is then responsible for constructing and providing the right kind of object graphs depending on those specifications.

Using Guice makes dependency injection nearly as easy to use as statically creating objects hierarchically the old-fashioned way. However, using Guice introduces a high level of blackbox magic, a non-trivial learning curve and has the nasty habit of moving what used to be compile time checked dependencies to runtime.

Most users of automated dependency injection have at least an uneasy ambivalence towards it, and some despise it with a passion programmers otherwise reserve for editors or programming languages... After heavily using Guice for a few years, I have come to accept and even recommend it as a reasonable standard tool for complex Java projects and a price to pay for the ability to more easily test and mock objects at any level of the hierarchy.