OOP Principles – Do IOC Containers Break OOP Principles?

inversion-of-controlobject-orientedsolid

What is the purpose of IOC Containers? The combined reasons for it can be simplified to the following:

When using OOP/SOLID Development principles, Dependency Injection gets messy. Either you have the top-level entry points managing dependencies for multiple levels below themselves and passing dependencies recursively through construction, or you have somewhat duplicate code in factory/builder patterns and interfaces that build dependencies as you need them.

There is no OOP/SOLID way to perform this AND have super pretty code.

If that previous statement is true, then how do IOC Containers do it? As far as I know, they aren’t employing some unknown technique that can't be done with manual D.I. So the only explination is that IOC Containers break OOP/SOLID Principles by using static objects private accessors.

Do IOC Containers break the following principles behind the scenes? This is the real question since I have a good understanding, but have a feeling somebody else has a better understanding:

  1. Scope control. Scope is the reason for nearly every decision I make on my code design. Block, Local, Module, Static/Global. Scope should be very explicit, as much at block-level and as few at Static as possible. You should see declarations, instantiations, and lifecycle endings. I trust the language and GC to manage scope as long as I’m explicit with it. In my research I’ve found that IOC Containers set up most or all dependencies as Static and control them through some AOP implementation behind the scenes. So nothing is transparent.
  2. Encapsulation. What is the purpose of encapsulation? Why should we keep private members so? For practical reasons it is so implementors of our API can’t break the implementation by changing the state (which should be managed by the owner class). But also, for security reasons, it’s so injections can’t occur that overtake our member states and bypass validation and class control. So anything (Mocking frameworks or IOC frameworks) that somehow injects code before compile time to allow external access to private members is pretty huge.
  3. Single Responsibility Principle. On the surface, IOC Containers seem to make things cleaner. But imagine how you would accomplish the same thing without the helper frameworks. You would have constructors with a dozen or so dependencies being passed in. That doesn’t mean cover it up with IOC Containers, it is a good thing! It’s a sign to re-factor your code and follow SRP.
  4. Open/Closed. Just like SRP isn’t Class-Only (I apply SRP down to single-responsibility lines, let alone methods). Open/Closed is not just a high level theory to not alter the code of a class. It’s a practice of understanding the configuration of your program and having control over what gets altered and what gets extended. IOC Containers can change the way your classes work altogether partially because:

    • a. The main code isn’t making the determination of switching out
      dependencies, the framework configuration is.

    • b. The scope could be altered at a time that isn’t controlled by the
      calling members, it is instead determined externally by a static
      framework.

So the configuration of the class isn’t really closed is it, it alters itself based on the configuration of a third party tool.

The reason this is a question is because I am not necessarily a master of all IOC Containers. And while the idea of an IOC Container is nice, they appear to just be a façade that covers up poor implementation. But in order to accomplish external Scope control, Private member access, and Lazy loading, a lot of non-trivial questionable things have to go on. AOP is great, but the way it is accomplished through IOC Containers is also questionable.

I can trust C# and the .NET GC to do what I expect it to. But I can’t put that same trust in a third party tool that is altering my compiled code to perform workarounds to things I wouldn’t be able to do manually.

E.G.: Entity Framework and other ORMs create strongly Typed objects and map them to database entities, as well as provide boiler-plate basic functionality to perform CRUD. Anybody could build their own ORM and continue to follow OOP/SOLID Principles manually. But those frameworks help us so we don’t have to re-invent the wheel every time. Whereas IOC Containers, it seems, help us purposely work around OOP/SOLID Principles and cover it up.

Best Answer

I'll go through your points numerically, but first, there's something you should be very careful of: don't conflate how a consumer uses a library with how the library is implemented. Good examples of this are Entity Framework (which you yourself cite as a good library) and ASP.NET's MVC. Both of these do an awful lot under the hood with, for example, reflection, which would absolutely not be considered good design if you spread it through day-to-day code.

These libraries are absolutely not "transparent" in how they work, or what they're doing behind-the-scenes. But that's not a detriment, because they support good programming principles in their consumers. So whenever you talk about a library like this, remember that as a consumer of a library, it's not your job to worry about its implementation or maintenance. You should only worry about how it helps or hinders the code you write which uses the library. Don't conflate these concepts!

So, to go through by point:

  1. Immediately we reach what I can only assume is an example of the above. You say that IOC containers set up most dependencies as static. Well, perhaps some implementation detail of how they work includes static storage (though given that they tend to have an instance of an object like Ninject's IKernel as their core storage, I doubt even that). But this is not your concern!

    Actually, an IoC container is just as explicit with scope as poor man's dependency injection. (I'm going to keep comparing IoC containers to poor man's DI because comparing them to no DI at all would be unfair and confusing. And, to be clear, I'm not using "poor man's DI" as a pejorative.)

    In poor man's DI, you'd instantiate a dependency manually, then inject it into the class that needs it. At the point where you construct the dependency, you'd choose what to do with it- store it in a locally scoped variable, a class variable, a static variable, don't store it at all, whatever. You could pass the same instance into lots of classes, or you could create a new one for each class. Whatever. The point is, to see which is happening, you look to the point- probably near the application root- where the dependency is created.

    Now what about an IoC container? Well, you do exactly the same! Again, going by Ninject terminology, you look at where the binding is set up, and find whether it says something like InTransientScope, InSingletonScope, or whatever. If anything this is potentially clearer, because you have code right there declaring its scope, rather than having to look through a method to track what happens to some object (an object may be scoped to a block, but is it used multiple times in that block or just once, for example). So maybe you're repelled by the idea of having to use a feature on the IoC container rather than a raw language feature to dictate scope, but as long as you trust your IoC library- which you should!- there's no real disadvantage to it.

  2. I still don't really know what you're talking about here. Do IoC containers look at private properties as part of their internal implementation? I don't know why they would, but again, if they do, it's not your concern how a library you're using is implemented.

    Or, maybe, they expose a capability like injecting into private setters? Honestly I've never encountered this, and I'm dubious about whether this really is a common feature. But even if it's there, it's a simple case of a tool that can be misused. Remember, even without an IoC container, it's but a few lines of Reflection code to access and modify a private property. It's something you should almost never do, but that doesn't mean .NET is bad for exposing the capability. If somebody so obviously and wildly misuses a tool, it's the person's fault, not the tool's.

  3. The ultimate point here is similar to 2. The vast majority of the time, IoC containers DO use the constructor! Setter injection is offered for very specific circumstances where, for particular reasons, constructor injection can't be used. Anybody who uses setter injection all the time to cover up how many dependencies are being passed in is massively absuing the tool. That's NOT the tool's fault, it's theirs.

    Now, if this was a really easy mistake to innocently make, and one that IoC containers encourage, then okay, maybe you'd have a point. It'd be like making every member of my class public then blaming other people when they modify things they shouldn't, right? But anybody who uses setter injection to cover up violations of SRP is either willfully ignoring or completely ignorant of basic design principles. It's unreasonable to lay the blame for this at the IoC container.

    That's especially true because it's also something you can do just as easily with poor man's DI:

    var myObject 
       = new MyTerriblyLargeObject { DependencyA = new Thing(), DependencyB = new Widget(), Dependency C = new Repository(), ... };
    

    So really, this worry seems completely orthogonal to whether or not an IoC container is used.

  4. Changing how your classes work together is not a violation of OCP. If it was, then all dependency inversion would be encouraging a violation of OCP. If this was the case, they wouldn't both be in the same SOLID acronym!

    Furthermore, neither points a) nor b) come close to having anything to do with OCP. I don't even know how to answer these with respect to OCP.

    The only thing I can guess is that you think OCP is something to do with behaviour not being altered at runtime, or about where in the code the lifecycle of dependencies is controlled from. It's not. OCP is about not having to modify existing code when requirements are added or change. It's all about writing code, not about how code you've already written is glued together (though, of course, loose coupling is an important part of achieving OCP).

And one final thing you say:

But I can’t put that same trust in a third party tool that is altering my compiled code to perform workarounds to things I wouldn’t be able to do manually.

Yes, you can. There is absolutely no reason for you to think that these tools- relied on by vast numbers of projects- are any more buggy or prone to unexpected behaviour than any other third-party library.

Addendum

I just noticed your intro paragraphs could use some addressing too. You sardonically say that IoC containers "aren’t employing some secret technique we’ve never heard of" to avoid messy, duplication-prone code to build a dependency graph. And you're quite right, what they're doing is actually addressing these things with the same basic techniques as we programmers always do.

Let me talk you through a hypothetical scenario. You, as a programmer, put together a large application, and at the entry point, where you're constructing your object graph, you notice you have quite messy code. There are quite a few classes that are used again and again, and every time you build one of those you have to build the whole chain of dependencies under them again. Plus you find you don't have any expressive way of declaring or controlling the lifecycle of dependencies, except with custom code for each one. Your code is unstructured and full of repetition. This is the messiness you talk about in your intro paragraph.

So first, you start to refactor a bit- where some repeated code is structured enough you pull it out into helper methods, and so on. But then you start to think- is this a problem that I could perhaps tackle in a general sense, one that isn't specific to this particular project but could help you in all your future projects?

So you sit down, and think about it, and decide that there should be a class that can resolve dependencies. And you sketch out what public methods it would need:

void Bind(Type interfaceType, Type concreteType, bool singleton);
T Resolve<T>();

Bind says "where you see a constructor argument of type interfaceType, pass in an instance of concreteType". The additional singleton parameter says whether to use the same instance of concreteType each time, or always make a new one.

Resolve will simply try to construct T with any constructor it can find whose arguments are all of types which have previously been bound. It can also call itself recursively to resolve the dependencies all the way down. If it can't resolve an instance because not everything has been bound, it throws an exception.

You can try implementing this yourself, and you'll find you need a bit of reflection, and some caching for the bindings where singleton is true, but certainly nothing drastic or horrifying. And once you're done- voila, you have the core of your very own IoC container! Is it really that scary? The only real difference between this and Ninject or StructureMap or Castle Windsor or whatever one you prefer is that those have a lot more functionality to cover the (many!) use cases where this basic version wouldn't be sufficient. But at its heart, what you have there is the essence of an IoC container.