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):
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 (
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!
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
Loggerclass, you could just as easily edit that
Loggerclass 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.