21

In the past year, I created a new system using Dependency Injection and an IOC container. This taught me a lot about DI!

However, even after learning the concepts and proper patterns, I consider it a challenge to decouple code and introduce an IOC container into a legacy application. The application is large enough to the point that a true implementation would be overwhelming. Even if the value was understood and the time was granted. Who's granted time for something like this??

The goal of course is to bring unit tests to the business logic!
Business logic that is intertwined with test-preventing database calls.

I've read the articles and I understand the dangers of Poor Man's Dependency Injection as described in this Los Techies article. I understand it does not truly decouple anything.
I understand that it can involve much system wide refactoring as implementations require new dependencies. I would not consider using it on a new project with any amount of size.

Question: Is it okay to use Poor Man's DI to introduce testability to a legacy application and start the ball rolling?

In addition, is using Poor Man's DI as a grass roots approach to true Dependency Injection a valuable way to educate on the need and benefits of the principle?

Can you refactor a method that has a database call dependency and abstract that call to behind an interface? Simply having that abstraction would make that that method testable since a mock implementation could be passed in via a constructor overload.

Down the road, once the effort gains supporters, the project could be updated to implement an IOC container and the constructors would be out there that take in the abstractions.

3

3 Answers 3

29

The critique about Poor Man's Injection in NerdDinner has less to do with whether or not you use a DI Container than it does about setting up your classes correctly.

In the article, they state that

public class SearchController : Controller {

    IDinnerRepository dinnerRepository;

    public SearchController() : this(new DinnerRepository()) { }

    public SearchController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
}

is incorrect because, while the first constructor does provide a convenient fallback mechanism for constructing the class, it also creates a tightly-bound dependency to DinnerRepository.

The correct remedy of course is not, as Los Techies suggests, to add a DI container, but rather to remove the offending constructor.

public class SearchController : Controller 
{
    IDinnerRepository dinnerRepository;

    public SearchController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
}

The remaining class now has its dependencies properly inverted. You're now free to inject those dependencies however you like.

6
  • Thanks for your thoughts! I understand the "offending constructor" and how we should avoid it. I understand that having this dependency does not decouple anything, but it DOES allow for unit tests. I'm on the early side of introducing DI and getting unit tests in place as quickly as possibly. I'm trying to avoid the complication of a DI/IOC Container or factory this early in the game. As mentioned, later when the support for DI grows, we could implement a Container and remove those "offending constructors".
    – Airn5475
    Commented Jan 17, 2018 at 14:50
  • 2
    The default constructor is not required for unit tests. You can hand whatever dependency you like to the class without it, including a mock object. Commented Jan 17, 2018 at 15:43
  • 2
    @Airn5475 be careful that you are not over abstracting too. Those tests should test meaningful behaviours - testing that an XService passes a parameter to an XRepository and returns what it gets back isn't overly useful to anyone and doesn't give you much in the way of extensibility either. Write your unit tests to test meaningful behaviour and ensure that your components are not so narrow that you're actually shifting all your logic to your composition root.
    – Ant P
    Commented Jan 17, 2018 at 15:49
  • I dont understand how including the offending constructor would help for unit tests , surely it does the opposite? Remove the offending constructor so the DI is implemented properly and pass in a mocked dependency when your instantiating the objects.
    – Rob
    Commented Sep 14, 2018 at 16:03
  • @Rob: It doesn't. It's just a convenient way for the default constructor to stand up a valid object. Commented Sep 14, 2018 at 16:43
24

You make a false assumption here over what "poor man's DI" is.

Creating a class that has a "shortcut" constructor that still creates coupling is not poor man's DI.

Not using a container, and creating all injections and mappings manually, is poor man's DI.

The term "poor man's DI" sounds like a bad thing to do. For that reason, the term "pure DI" is encouraged these days as it sounds more positive and actually more accurately describes the process.

Not only is it absolutely fine to use poor man's/pure DI to introduce DI into an existing app, it is also a valid way of using DI for many new applications. And as you say, everyone should use pure DI on at least one project to really understand how DI works, before handling responsibility over to the "magic" of an IoC container.

4
  • 11
    "Poor man's" or "Pure" DI, whichever you prefer, also mitigates a lot of the problems with IoC containers. I used to use IoC containers for everything but the more time passes the more I prefer avoiding them altogether.
    – Ant P
    Commented Jan 17, 2018 at 15:38
  • 8
    @AntP, I'm with you: I'm 100% pure DI these days and actively avoid containers. But I (we) are in a minority on that score as far as I can tell.
    – David Arno
    Commented Jan 17, 2018 at 15:39
  • 2
    Seems so sadly - as I move away from the "magic" camp of IoC containers, mocking libraries and so forth and find myself embracing OOP, encapsulation, hand-rolled test doubles and state verification, it seems the trend for magic is still on the up. +1 anyway.
    – Ant P
    Commented Jan 17, 2018 at 15:42
  • 3
    @AntP & David: It's good to hear I'm not alone in the 'Pure DI' camp. DI containers were created to address deficiencies in languages. They add value in that sense. But in my mind, the real answer is to fix the languages (or move to other languages) and not to build cruft on top of them.
    – JimmyJames
    Commented Jan 17, 2018 at 18:23
0

Paragidm shifts in legacy teams/code bases are extremely risky:

Any time you suggest "improvements" to legacy code and there are "legacy" programmers on the team, you are just telling everyone that "they did it wrong" and you become The Enemy for the rest of your/their time with the company.

Using a DI Framework as a hammer to smash all the nails will just make legacy code worse than better in all cases. It is also extremely risky personally.

Even in the most limited cases like just so it can be used in test cases will just make those test cases "non-standard" and "foreign" code that will at best just get marked @Ignore when they break or worse get complained about constantly by the legacy programmers with the most clout with management and get re-written "correctly" with all this "wasted time on unit tests" blamed solely on you.

Introducing a DI framework, or even the concept of "Pure DI" to a line of business app, much less a huge legacy code base without the by off of management, the team and especially sponsorship of the lead developer will just be the death knell for you socially and politically on the team/company. Doing things like this is extremely risky and can be political suicide in the worst way.

Dependency Injection is a solution looking for a problem.

Any language with constructors by definition uses dependency injection by convention if you know what you are doing and understand how to properly use a constructor, this is just good design.

Dependency injection is only useful in small doses for a very narrow range of things:

  • Things that change a lot or have lots of alternative implementations that are statically bound.

    • JDBC drivers are a perfect example.
    • HTTP clients that might vary from platform to platform.
    • Logging systems that vary by platform.
  • Plugin systems that have configurable plugins that can be defined in configuration code in your framework and discovered automatically at startup and dynamically loaded/reloaded while the program is running.

12
  • 14
    The killer app for DI is unit testing. As you've pointed out, most software won't need a lot of DI by itself. But tests are also a client of this code, so we should design for testability. In particular, this means placing seams in our design where the tests can observe the behaviour. This doesn't require a fancy DI framework, but it does require that relevant dependencies are made explicit and substitutable.
    – amon
    Commented Jan 17, 2018 at 7:36
  • 4
    If us devs knew if advance what could change, that would be half the battle. I've been on long developments where a lot of supposedly "fixed" stuff changed. Future proofing here and there using DI is no bad thing. Using it everywhere all the time smacks of cargo cult programming.
    – Robbie Dee
    Commented Jan 17, 2018 at 13:43
  • 6
    @JarrodRoberson, that really is a toxic environment. One of the key signs of a good developer is that they look back at code they wrote in the past and think, "ugh, did I really write that? That is so wrong!" That's because they have grown as a developer and learned new things. Someone who looks at code they wrote five years ago and sees nothing wrong with it has learned nothing in those five years. So if telling folk they did it wrong in the past makes you an enemy, then flee that company fast, for they are a dead end team that will hold you back and reduce you to their poor level.
    – David Arno
    Commented Jan 17, 2018 at 18:19
  • 1
    Not sure I agree with the bad guy stuff but for me the key takeaway from this is that you don't need DI in order to do unit testing. Implementing DI to make code more testable is just patching over the problem.
    – Ant P
    Commented Jan 18, 2018 at 17:21
  • 1
    Even if the legacy developers disagree, your goal should always be to improve. Also without DI, some code can not be unit tested at all. But I guess that shows the problem you were refering too.
    – Tigerware
    Commented Aug 28, 2018 at 8:36

Not the answer you're looking for? Browse other questions tagged or ask your own question.