Dependency Injection

Dependency Injection (DI) for me always used to be this mouthful of a term I read about in wordy and boring Java enterprise articles. The last couple of months I ignored my bad association with DI and investigated it in the form of Dagger for Android applications. After the initial learning curve I even found useful potential applications of DI for our code base at work. So in this wordy and boring article I present my very own findings about DI. More precisely, I emphasize some aspects of DI that I found many articles glossing over too much, i.e. the motivation for DI frameworks, DI’s applications apart from testing and DI’s trade-offs, and I touch upon the relationship between DI and certain other programming language concepts.

Overview

As it turns out, DI itself is a big word for a simple idea. To put it very bluntly for Java: “Make every object in a class a constructor parameter”. A more delicate explanation goes something like this. The initial, non-DI situation is that of a class C with local and fixed dependencies that cannot be exchanged from outside. In such a situation coupling between C and its dependencies is strong and the ability to change these dependencies for an instance of C by a client (aside from direct manipulation of the source code) is practically impossible. To make C now adhere to the DI-principle these fixed dependencies can simply be parametrized into constructor parameters resulting in C’. Coupling is reduced a lot as C’ now only depends on the interfaces of its dependencies but not on their concrete implementations (hopefully). Conversely, changeability or flexibility of C’ is increased. 1

Let us give our, so far quite featureless, non-DI class C and DI class C’ each a face and illustrate the point of DI with a concrete, albeit artificial, example.

class C {
   D d;
   C() { d = new D(); }
}

class C {
   D d;
   C(D dependency) { d = dependency; }
}

Let D1 and D2 be two new classes that each inherit from D. We now see the advantage of DI. Namely, C’ can readily be instantiated with an instance of D1 in one place and an instance of D2 in another. The same is not true for C. 2

class D1 extends D { /* ... */ }
class D2 extends D { /* ... */ }
// ...
C c1 = new C(new D1());
// ...
C c2 = new C(new D2());

DI sounds great, does it not? There is a problem though. Let us imagine that C’ has many more constructor parameters. Its constructor arguments need to be manually and explicitly initialized, configured and passed on for each construction site (at least in pure Java). Worse yet, if all our classes follow the DI-principle there is a “transitive initialization explosion”. Because understanding this point is crucial, let me stress it with a concrete, although again artificial, example. 3

We follow the DI principle and now face the task of creating an instance x of the class X whose constructor merely has four dependencies.

X x = new X(x1, x2, x3, x4);

But we still need to initialize x1, x2, x3 and x4. Let us begin with x4.

X4 x4 = new X4(x41, x42);
X x = new X(x1, x2, x3, x4);

But we still need to initialize x4’s dependencies before, i.e. in the correct order.

// ...
X3 x3 = new X3(x31, x32, x33);
X42 x42 = new X42();
X41 x41 = new X41();
X4 x4 = new X4(x41, x42);
X x = new X(x1, x2, x3, x4);

Clearly, this mechanical accounting is a great reduction in programming comfort and at some point probably unsustainable. 4 5

DI frameworks, as opposed to the mere concept behind DI, mainly exist for just the purpose to make it possible to adhere to DI and thus profit from increased changeability without making a great compromise in programming comfort or convenience. They take the burden of the mechanical accounting from the programmer. They are neither evil nor some unjustified, over-engineered crap—their existence is reasonably motivated. Granted, a big upfront learning effort is required to get going with a DI framework and like every practical framework most of them are not perfect and must make many compromises. These initial costs and possible deficits are more than amortized in a sufficiently complex project though.

A side mark about understanding DI. I personally never found that the Wikipedia article, Martin Fowler’s article or even the main results for a “Dependency Injection” Google search really made it click for me what all the fuss about DI is. What helped me not only understand the DI principle—which is relatively straight-forward—but rather its consequences are the following video presentations. 6

Flavors

One of the main advantages which is touted in many DI articles is that DI makes for easier testing. Other touted advantages may include: comfort of static variables without guilty conscience, better prepared to incorporate changes (e.g. replacing database software), real modularization and better adhering to the single responsibility principle. These are all great advantages and much has been written about them so I do not want to repeat what you can find easily elsewhere. Instead, I want to emphasize a benefit that directly follows from the increased changeability DI introduces: the improved ability to create different flavors from a common code base. If everything is exchangeable then it is easy to replace a certain part to create a desired customization. 7 8 9

How come this aspect of DI interests me? You see, at work we face the problem to maintain different, customized versions of the same software for multiple clients. Our current solution is roughly and simplified the following. We have a base repository which is the uncostumized flavor. Each customized project is a fork of the base repository and receives its respective custom changes. Every couple of days the base repository is merged into the customized repository so that it receives improvements and bug fixes from the base. Cercerilla and Htbaa from the above thread essentially explain our solution. We do not use this system very long yet but from what I can tell it works alright. But to be honest it irks me a bit because I know there is a “better” solution to our problem and it involves DI.

I will just quote Alb’s answer because he explains this DI-approach well.

Don’t do this with SCM branches. Make the common code a separate project that produces a library or project skeleton artifact. Each customer project is a separate project which then depends on the common one as a dependency. This is easiest if your common base app uses a dependency injection framework like Spring, so that you can easily inject different replacement variants of objects in each customer project as custom features are required. Even if you don’t already have a DI framework, adding one and doing it this way may be your least painful choice.

But why exactly is the DI-approach with the common code base as a library “superior” to SCM deltas? The linked thread gives some potential answers, for example quickly_now‘s objection that “you still have to maintain all those deltas in source control and keep them lined up—forever. Short term this might work. Long term it might be terrible”. However, I believe the main disadvantage of the branch-based approach compared to the DI-librarized approach is that the former loses vital structural information because it works on the abstraction level of text and not on the richer syntax and semantics of the employed programming language. By using DI and (interface) inheritance there are more useful constraints to be obeyed to and which prevent certain classes of mistakes. Looking at this point from another perspective one could also say that this library-based approach forces one much more to consider how the library is going to be used which hopefully will lead to a more useful design. 10 11 12

The DI-based library approach is sadly not a silver-bullet either because of “logical” conflicts or rather backwards-incompatible changes. For example, identifier renamings, changes to pre-/post-conditions or invariants and signature changes in the library may make certain flavors stop working correctly after an update even if there were not any compiler errors. The golden approach—which is taken by many open-source projects—to mitigate these “dangerously silent” changes is to have a changelog in order to prepare clients of the library of possible problems and simply following a cautious way of introducing these changes by not just removing old APIs or drastically changing their behavior but instead marking them as deprecated and introducing alternative, new APIs. Still, I believe it is the best current solution to provide different customer flavors from a common code base in general—for each specific situation you have to weigh in all the aspects, of course, and other solutions might be a better deal then. 13

Trade-offs

Following DI does not automatically mean you should parametrize every single dependency of a class. Only do this if you feel you gain something out of it. Of course, feeling and knowing are worlds apart so it is a tough call to make. Deciding on trade-offs is never easy but it is a big part of being a developer.

Restructuring a big, actively maintained non-DI code base to follow DI principles is an enormous undertaking. The upfront learning curve needed to get proficient with a DI framework is quite big as well. There is more scaffolding and probably a bit more boilerplate needed for a project. These (initial) costs should never be underestimated and depending on the scope and the longevity of a project they might never pay off.

The increased changeability that is gained with DI is payed for with less stability or rather predictability. As the implementor of a DI-based library you give up control. This is a fundamental trade-off that is also related to encapsulation. To learn more about this trade-off simply search Google for “dependency injection encapsulation” and you will surely discover this remarkable article. You have to pose yourself the question what is more important to you, changeability or stability. I would say that for the vast majority of (end-user) software projects the ability to quickly adapt to changed requirements is worth more than stability.

Relation to Other Concepts

In this section I merely slightly touch upon DI’s relationship to other computer science, programming or programming language concepts as a more thorough treatise would go beyond the scope of this article. I find this topic very interesting though, so I hope to publish a thorough article about it in the future.

  • We have seen in the previous section that there is a fundamental tension between encapsulation and DI.
  • Usages of DI intersect with potential usages of global variables, singletons or static fields and dynamic scoping.
  • In DI frameworks there is the concept of modules which are essentially factories. In general, DI and DI frameworks are related to the static and abstract factory pattern and the service locater pattern.
  • The following is a quote by Gilad Bracha from his article Constructors Considered Harmful: “DI frameworks are just a work around for a deficiency in the underlying language.” This statement is very interesting and naturally raises the question what native solutions other non-mainstream languages have to offer to tackle the same problem that DI frameworks tackle. For example, in Scala there is the cake pattern.

Conclusion

In this article we have learned that there is a difference between the idea of DI and DI frameworks: namely convenience. The idea of DI is simple. Yet its consequences are extensive. Among other useful applications having the ability to create customized flavors from a common code base is one of DI’s particular strengths. Even though DI has many benefits there are trade-offs to be made and depending on the specific situation DI might not be worth it—but it very often is. DI intersects other programming related concepts and the need to use DI frameworks can be seen as a “deficiency in the underlying language”. 14


  1. In a more abstract sense, DI means adding another layer of indirection to your “enclosed entity”. This layer contains links to other entities and can be configured individually for each entity which ultimately results in greater changeability or flexibility of the software. The concept of “additional indirection to gain flexibility” is prevalent in computer science, for example in the form of pointers, higher-order functions, callbacks, dynamic dispatch and factory methods. On the other hand, more flexibility is in general paid for with less stability (or rather predictability and often performance, too). But for most practical software projects changeability is worth much more than stability. As an aside, I invented the term “enclosed entity” on the spot simply to make it clear that I am not necessarily talking about objects in the OOP sense. 

  2. One could maybe come up with the doomed idea to replace C’s constructor body with d = new D1(). However, all instances of C would then be initialized with D1 and it still would not be possible to choose the dependency for each instance of C

  3. This “transitive initialization explosion” is related to the phenomena “combinatorial explosion”

  4. This problem exists mainly due to the transitivity of the dependencies which can be conceptualized as a big directed acyclic graph (DAG). As an aside, DAG is the inspiration for the name Dagger. In one of the linked videos the presenter says that in a Google Android project there were 3000 lines of code just for this accounting and in a backend project there were around 100000 lines of code before the switch to a DI framework… 

  5. Some people apparently have no problem with the reduced programming comfort and gladly trade this pain for the pain of using a DI framework: http://www.yegor256.com/2014/10/03/di-containers-are-evil.html#the-right-way Personally, I would always choose the DI framework as I consider its pain much smaller. 

  6. I must admit that I read a lot about DI before it finally made sense to me so these videos might not be enough to make it click for someone totally new to DI. Also, I played with Dagger in practice and formulated questions that I put on e.g. Stackoverflow so that helped me, too. On the other hand, I am a slow understander anyway. Your mileage may vary. 

  7. You may sometimes get the impression that DI is only useful for “services”. E.g. the ability to exchange one database provider with another in your software. But this is “just” a special case of DI. Nevertheless, as I will explain further in the section “Trade-offs”, using DI only for services and nothing more might be the sweet-spot for DI or, put differently, the “best” trade-off between changeability and stability for a software project. 

  8. I am using “flavors” for lack of a better term. I guess I could have also used the terms types, variants, versions, forms, configurations or manifestations. I hope it is still clear what I mean by flavors. 

  9. The testability advantage can be understood from this flavor perspective. Namely, one is able to provide a special flavor of the common code base which simply has the characteristic that it is suitable for testing (“mock-mode”). 

  10. In practice it might not even be superior because of justified time- and resource-based counter arguments. Even just restructuring the application after DI and having developers invest a great learning effort into DI and a DI framework might not pay off in the short- and mid-term and possibly not even in the long-term if the differences between the flavors are rather shallow. 

  11. Also, merge conflicts when merging the base project into the customized project simply will not happen with the library-based approach because this activity itself simply vanishes. 

  12. What about not using DI and inheritance but instead one big project with if statements littered through the code base of the form if (isCustomer1) then doCustomer1Stuff()? Well, it certainly works on the abstraction level of the language’s syntax and semantics. But it has many disadvantages. For example the code of a class/method/statement body mixes many different concerns and becomes hard to follow. So this approach should not be used. Note that pre-processor if-defs work on the abstraction level of text and so are “even worse” but certainly good enough for non-complex tasks. 

  13. Also, there is a tension with encapsulation, the granularity of classes and DI. Because the more exposed and smaller your classes are the easier it is to inject just a small new class into the base library to achieve a desired change. If the classes are bigger or have many private methods you may have to recreate (or copy&paste) much functionality before you can inject. But encapsulation is good, right? Learn more in the next section. 

  14. Much more can be said about DI. A historical analysis would be interesting for example. Even though I tried to keep scope of article small it still became relatively large. I hope I could shed some light on some otherwise weakly illuminated points about DI