3

I get that IOC containers can be useful to help break dependencies and allow you to test a class in isolation. I don't wish to focus on that right now, instead, I'm trying to understand some of the other arguments for using an IOC container. There's quite a few articles out there that introduce IOC containers (and the general pattern of dependency injection), not as a tool to help make code more testable, but as a tool to make your classes less coupled to each other. And I'm really struggling to see how IOC containers help with any of this.

The arguments usually go something like this (here's one such concrete example):

Using new Logger() makes you coupled to that specific Logger class, which is bad. What if, in the future, you need to support many different types of logging behaviors, like file-system logging and logging-over-network?

The solution? Use dependency injection (and IOC containers). Make your class accept any generic ILogger type in its constructor, and then you can provide whatever dependency it needs (FileSystemLogger or NetworkLogger) without changing anyone's implementations. I mean, think of all of the refactoring you would have to otherwise do to change each new Logger() into some sort of LoggerProvider.create() for this to work!

Sounds great!

So, um, what exactly do these people mean when they say they need to "support multiple different types of loggers"? From my understanding, IOC containers typically only allow you to wire up one concrete class for a given interface. So, there's a few ways I could try to interpret this problem:

  • If we're imagining that we are using an IOC container that only provides the very basic functionality of 1-to-1 mapping an interface to a concrete class, and if we were to assume that this particular IOC container only supports wiring up dependencies programmatically at the root of your project, then I'm at a complete lost as to how you would support multiple logger classes. Maybe the FileSystemLogger is just dead code and you've permanently switched to using NetworkLogger instead? But you keep the FileSystemLogger around just in case you need to switch back? This sounds absurd - in the non-IOC world where you just have a single Logger class, you could just as easily edit that Logger class to have network-logging behaviors instead of file-system-logging behaviors, and leave in a giant comment the old implementation so you can switch back if you ever need to (or, even better, just rely on source control keeping a record of the old implementation).

  • Ok, but maybe our IOC supports XML-based configuration. This means we can create different builds of the project with different behaviors by simply swapping out the XML file being used. Which, alright, fine, I could kind-of sort-of see that, but at the same time, not really. In this picture, we're making it so every single one of our classes supports dependency injection so that if we ever want to make multiple builds with multiple implementations of any specific class, we can do it? That feels like overkill. And what if our requirements change tomorrow and now the logging behavior needs to be configured via a user-facing config file that the application reads during start-up? Well, phooey, our fancy IOC container can't support a simple use-case like that, now we've got to figure something else out. And what about those IOC containers that don't support XML-based configuration - is it just an anti-pattern to use those, because they fail to provide the "main" feature these containers exist for? This is all beginning to sound ludicrous.

  • Maybe our IOC container supports the ability to configure how things are wired up at runtime? That would certainly help with the "what if you need to configure your logger type at runtime" argument. Or maybe the IOC container supports providing one implementation for one portion of the application and another implementation for another portion? Or maybe it has some other special feature that I'm not thinking of. But these are all extra nice-to-have features of IOC containers, these don't feel like they're core to what an IOC is. If any of this stuff is at its core, I would expect all IOC containers to support these features.

So, what in the world does this "support multiple loggers" use case even mean?

Edit: I should clarify that I'm mostly focusing on IOC containers in this question, because the principle of "dependency inversion" itself is very flexible. E.g. one could use dependency inversion very selectively, here and there, where you feel it's needed. IOC containers are interesting because they are basically prescribing the use of dependency inversion almost everywhere.

4 Answers 4

8

Bullet point 1

The question isn't so much "how can I have multiple kinds of loggers in my runtime". The question is "across the entire lifetime of my codebase, how can I ensure that if I want to change my logging implementation, I don't have to spend even more time rewriting other components just because they interacted with the old logging implementation".

The focus here is the ability to change a specific part of your codebase without needing to change others, and continually minimizing the amount of code you have to touch to introduce a particular change.

Bullet point 2

If you have a test suite, you will continually make use of different builds where the injected dependencies are different, i.e. you inject the real dependency in your real production deployment, and you inject a mocked dependency during your automated test runs.

Furthermore, clean coding generally costs you some additional upfront effort. You're not wrong that implementing IOC does indeed cost more effort as opposed to not bothering.
However, having your codebase be IOC-friendly will pay back significantly longer dividends in the long run. It's not about the time to push out a first version, it's about the ease of maintaining the product in the long run. Clean coding sacrifices the former (at least in the initial stages when the developers aren't confident with clean coding habits) for the sake of the latter. Slow and steady truly wins the race here.

Bullet point 3

You can definitely implement something like that if you need it. But it's not the main reason to do IOC/DI.

9
  • Re point 1 - if I want to change my logging implementation, in a non-dep-inject app I just edit one place - the Logging file. I'm still confused at what IOC gives me in this regard. Commented Oct 3, 2023 at 16:38
  • @ScottyJamison: If your response to coding guidelines is going to be of the type "there's other ways I could achieve the same thing" (which is fine) and you won't take someone's word for it blindly about the benefits (testability, maintainability, reducing future change impact,...) (which is also fine), then the only way to address your question would be by doing an in-depth study of a team's efforts and what they work on, the estimates of implementing future changes that weren't expected during initial development, regressions and overall bugs, ... that would be an extensive case study.
    – Flater
    Commented Oct 3, 2023 at 21:51
  • @ScottyJamison: I don't believe in dogma, and I do understand that my position appears dogmatic to you. The short and simple answer is that many experts and senior developers all advocate for clean coding specifically because they've lived through those extensive case studies (i.e. really bad places to work at). It's perfectly reasonable for you to want to understand something from the ground up and not take others' word for it, but do consider whether the benefit of doing so will justify the cost of having to go the long way round.
    – Flater
    Commented Oct 3, 2023 at 21:53
  • 1
    @ScottyJamison: If you prefer a concrete example: What if you have a second customer who needs different logging, but you still need your classic logging for your first customer? If you copy/paste and separate your codebases, how will you synchronize feature work that applies to both customers? What if there's a third customer who wants to have another difference? Every time someone introduces a variation that you did not think of, you will need to rewrite the things that make use of that implementation. With IOC, you don't. You just add a different implementation and swap it out.
    – Flater
    Commented Oct 3, 2023 at 22:00
  • Sorry if it came across like that, but my intention isn't to try and fight good coding standards, nor do I believe you're doing this because of dogma. Honestly, there just seems to be something very fundamental that I'm missing, and I just don't know what piece I'm missing. I'm not looking for proof of what is better and what isn't, I'm just trying to figure out where people are coming from. And I'm still not quite getting it yet, hence the further questions. Commented Oct 3, 2023 at 22:01
2

I think the thing you are missing is that in a lot of cases the classes you write are actually used by someone else.

So say you have written:

public class Calculator
{
    Calculator()
    {
      this.logger = new MyLogger();
    }
    int Add(int a, int b)
    {
       this.logger.Log("running the add function!");
    }
}

Someone else, let's call them Bob, wants to use you amazing calculator class. BUT! they have a requirement that when they log they need to add a timestamp to every line.

Because you haven't exposed the Logger class, Bob can't change it and so cant use your calculator and has to go write their own.

Or worse case, Bob gets your source code and changes it, breaking all the other programs that use your calculator!

Now, if you had written:

public class Calculator
{
    Calculator(Ilogger logger)
    {
      this.logger = logger;
    }
    int Add(int a, int b)
    {
       this.logger.Log("running the add function!");
    }
}

Now Bob can write their own logger class, inheriting from the interface, inject it into your class and everyone is happy.

Other programmers Jane and Freddy have similar differing requirements about the logger in their applications, so in the world, there are four programs that use your calculator class and four different logger implementations.

Obviously each individual program only uses a single concrete logger class.

PS. I should say something about IoC containers. They aren't really relevant to DI or your question, but! Imagine a world where everyone writes classes like the second example. Instancing classes is a pain, you have to write new MyCalculator(new Logger(new FileSystem(new ColourPicker()))) all over the place. But then someone invents IoC containers to neaten that up for you. Now everyone also uses IoC containers to do DI

9
  • Interesting perspective. So, are you suggesting that a library makes it so pretty much all their classes can be custom-wired-up by the library consumer? That means, as the library author, I can never add new methods to any existing class/interface without it being considered a breaking change, since someone could be swapping out my class for their own and their class may not conform to the updated interface? So we're giving tons of flexibility to the library consumer, just in case they need it, but we're also making the library itself extremely rigid. Commented Oct 3, 2023 at 20:47
  • @ScottyJamison both the author and consumer adding new methods doesn't prevent backwards compatibility. Just means you each need your own namespace. Commented Oct 3, 2023 at 21:06
  • If I want to add a new Flush() method to the logger class and interface, then any end-user who was trying to implement my Logger interface would need to update their classes to include Flush() as well, which is why its a breaking change. If my library only has a couple of explicit and intentional places that support this kind of "class swappability", then that seems fine, but if every single class is intended to be substitutable by the end user if they so wish, then that seems bad. Commented Oct 3, 2023 at 21:52
  • if you add Flush() to the interface, and the consumers get the new version of your class, they can then update their own implementations and everything works. Yes it would be a breaking change, but so what?
    – Ewan
    Commented Oct 3, 2023 at 22:10
  • 1
    This is the right answer. @ScottyJamison As for "spilling the guts of the library" That is indeed risk that you, as library developer need to consider. Expanding API surface to give users of your library/framework more flexibility must be weighted against risk of creating bad API and getting stuck with it. It is also why API design is such a big deal.
    – Euphoric
    Commented Jul 25 at 6:54
0

Slightly old question, but here are a few practical examples:

  1. My product runs on an Oracle database - I want to swap that for PostgreSQL, and I can do that by swapping the database driver, and editing a few config files. I don't need to rewrite the code to replace a bunch of Oracle-specific classes with Postgres-specific classes.

  2. My product currently runs in an application server with a database connection pool provided by the application server. I also want to run some of the code in a standalone environment, so I need to provide a simple replacement, such that the code just gets connections without worrying where they came from.

  3. My product interacts with a messaging system, and currently uses one provided by the application server. If I want to swap it for a different messaging system - e.g. because the client has something they prefer - that's much easier if I can just drop in an adapter that hides everything behind a nice "send message / receive message" interface.

0

So, what in the world does this "support multiple loggers" use case even mean?

Work with abstractions, the way interface is, to declare beans' dependencies, that is variables, and with instances identifiers, the way qualifier is, to choose between the available implementations of the dependencies at startup time, run time or whenever a choice is to be done.

Maybe you are wondering "what IOC containers even do?". If not ignore the rest of this post, though if you are at this stage with the reading IOC containers are a table of content of factories for available classes, factories that could be configured at development phase or discovered at startup time would be a birds eye view over the IOC container.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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