Basically, dependency injection makes some (usually but not always valid) assumptions about the nature of your objects. If those are wrong, DI may not be the best solution:
First, most basically, DI assumes that tight coupling of object implementations is ALWAYS bad. This is the essence of the Dependency Inversion Principle: "a dependency should never be made upon a concretion; only upon an abstraction".
This closes the dependent object to change based on a change to the concrete implementation; a class depending upon ConsoleWriter specifically will need to change if output needs to go to a file instead, but if the class were dependent only on an IWriter exposing a Write() method, we can replace the ConsoleWriter currently being used with a FileWriter and our dependent class wouldn't know the difference (Liskhov Substitution Principle).
However, a design can NEVER be closed to all types of change; if the design of the IWriter interface itself changes, to add a parameter to Write(), an extra code object (the IWriter interface) must now be changed, on top of the implementation object/method and its usage(s). If changes in the actual interface are more likely than changes to the implementation of said interface, loose coupling (and DI-ing loosely-coupled dependencies) can cause more problems than it solves.
Second, and corollary, DI assumes that the dependent class is NEVER a good place to create a dependency. This goes to the Single Responsibility Principle; if you have code which creates a dependency and also uses it, then there are two reasons the dependent class may have to change (a change to the usage OR the implementation), violating SRP.
However, again, adding layers of indirection for DI can be a solution to a problem that doesn't exist; if it is logical to encapsulate logic in a dependency, but that logic is the only such implementation of a dependency, then it is more painful to code the loosely-coupled resolution of the dependency (injection, service location, factory) than it would be to just use new
and forget about it.
Lastly, DI by its nature centralizes knowledge of all dependencies AND their implementations. This increases the number of references that the assembly which performs the injection must have, and in most cases does NOT reduce the number of references required by actual dependent classes' assemblies.
SOMETHING, SOMEWHERE, must have knowledge of the dependent, the dependency interface, and the dependency implementation in order to "connect the dots" and satisfy that dependency. DI tends to place all that knowledge at a very high level, either in an IoC container, or in the code that creates "main" objects such as the main form or Controller which must hydrate (or provide factory methods for) the dependencies. This can put a lot of necessarily tightly-coupled code and a lot of assembly references at high levels of your app, which only needs this knowledge in order to "hide" it from the actual dependent classes (which from a very basic perspective is the best place to have this knowledge; where it's used).
It also normally doesn't remove said references from lower down in code; a dependent must still reference the library containing the interface for its dependency, which is in one of three places:
- all in a single "Interfaces" assembly that becomes very application-centric,
- each one alongside the primary implementation(s), removing the advantage of not having to recompile dependents when dependencies change, or
- one or two apiece in highly-cohesive assemblies, which bloats the assembly count, dramatically increases "full build" times and decreases application performance.
All of this, again to solve a problem in places where there may be none.
Best Answer
I was Editor of Software Development magazine when the Gang of Four book came out and I can say with total confidence that unit-testing was not a widespread practice in 1994, when Design Patterns was originally published.
In 1994, C++ was the most commonly used object-oriented language, and most people programming it were coming from a C background. One of the "thinking in objects" things that people simply didn't have is the idea of hundreds or thousands of entry points into your program. You thought about the
main()
. If you worked on a large project, you might have a (usually quite elaborate) makefile to create a module-based program. But "unit-testing"? Starting a process, building the necessary memory context, executing it, and tearing it down, on a per method basis? That was very radical.Java made multiple-entry-point programming more obvious. By the time of the original Dot-Com boom, unit-testing was a well-known technique, but it was really JUnit (circa 2001?) that caused it to catch fire and become a universal practice.
Although Strategy and the general concept of programming to an interface were part of GoF and the mid-90s zeitgeist, the idea of injection came quite late to the party (circa '03-'05?). Honestly, my gray hairs are still quite dubious about that aspect of DI ("Get off my lawn, you darn configuration files!").