No DI Container | DI Container | |
---|---|---|
No DI | Spaghetti Code | Service Locator Anti-Pattern |
DI | Manual Dependency Injection | Enhanced Dependency Injection |
Some took exception with the characterization of non-DI code as spaghetti code. My intent was not to be derogatory but was to succinctly describe the increased complexity of the control flow and the increased coupling of dependencies that occurs without dependency injection (due to the mixing of creation and logic concerns). This post examines the use of this label and also contains an invitation to suggest an alternate metaphor for describing the structure of non-DI code in the context of an explanation of dependency injection.
Wikipedia describes spaghetti code as "a pejorative term for source code that has a complex and tangled control structure." I hadn't intended to be uncomplimentary, but I do assert that the mixing of concerns caused by the lack of DI does indeed create a more complex and tangled flow. Without dependency injection:
- creation concerns are mixed with the control flow (that is, with the application logic)
- object lifetime concerns are mixed with the control flow
Because creation instructions unrelated to the application logic are mixed in with the control flow, the control flow is (in my opinion) more complex and tangled without DI than with DI. However, 'more complex and tangled than X' doesn't necessarily mean 'complex and tangled'. So at what point does the mixing of concerns or additional complexity move from acceptable to spaghetti code? Is the increased complexity in flow without DI enough by itself to merit term spaghetti code? How many responsibilities can a unit of code have before turning into spaghetti? How tight can couplings become before they are too tight? I don't think that these questions can be answered outside of an individual's judgement within a particular context. For my context in the original post of a high level comparison of the structural differences between DI code and non-DI code, I found the metaphor illustrative.
I also consider it more likely that without DI there will be increased flow complexity and dependency complexity due to reliance on global state (such as GoF-style singletons) and service locators. Without a context-independent dependency management strategy like DI provides, I feel that these problematic dependency management strategies are more likely to be used (and hence mixed in with the logic). This is of course not a given, however.
Beyond control flow concerns, I find it illustrative to use a spaghetti code metaphor in relation to object graphs. When the dominant paradigm was procedural, it made sense to define spaghetti code strictly in terms of control structure. But do people really write (for example) many goto statements these days? Now that object-oriented programming is the dominant paradigm, high coupling between components seems to be a more common problem than, say, unstructured branching. Of course, complexity in OO control flow could be considered a side-effect of high-coupling between objects since the objects in a system express the control structure. Use of a spaghetti code metaphor is entirely appropriate in my opinion for describing high, unnecessary coupling among objects.
Is it possible to write good code without DI? Of course. People have been doing that for a long time and will continue to do so. Might it be worth making a design decision to accept the increased complexity of not using DI in order to maximize a different design consideration? Absolutely. Design is all about tradeoffs. But the point is that this should be a conscious decision informed by an understanding of what is being lost and what is being gained.
It is of course possible to write spaghetti code with DI too. I did not mention this in the original post because it was about contrasting properly-applied DI with other alternatives and was not about describing poorly-applied DI. My impression is that improperly-applied DI leads to worse spaghetti code than non-DI code. It's essential to understand guidelines for injection in order to avoid creating additional dependencies. Misapplied DI seems to involve more problems than not using DI at all.
I suspect that much of the antipathy for dependency injection stems from experience with poorly-applied DI. Someone might start on a project where DI has been misapplied or where there has been 'container abuse' and then conclude that DI is nothing but hype and that its practitioners are a cargo-cult. It would be understandable in that situation to not distinguish between the baby and the bath water, but it would be a mistake to ignore a valuable tool because of the result of its misapplication. If someone attempts to use a lawnmower to paint a house, should they then conclude that lawnmowers have no value after viewing the result?
In any case, my purpose in using the term 'spaghetti code' was to illustrate the structural differences between a non-DI approach and a DI approach for those who may not yet understand the difference. I think the metaphor does work in this context, but the term is indeed pejorative. If you have a metaphor that illuminates this difference without being uncomplimentary, please leave a comment. I'm interested in suggestions.