DaedTech

Stories about Software

By

Encapsulation vs Inversion of Control

This is a post that I’ve had in my drafts folder for nearly two years. Well, I should say I’ve had the title and a few haphazard notes in my drafts folder for nearly two years; I’m writing the actual post right now. The reason I’ve had it sitting around for so long is twofold: (1) it’s sort of a subjective, tricky topic and (2) I’ve struggled to find a concrete stand to take. But, I think it’s relatively important, so I’ve always vowed to circle back to it and, well, here we are.

The draft was first conceived when I’d given a presentation about testability and inversion of control — specifically, using dependency injection via constructors and setters to achieve these ends. I talked, among other things about the Open/Closed Principle, and how this allows modifications to system behavior that favor adding over editing code. The idea here is that we can achieve new functionality with a minimum of violence to the code base and in a way for which it is easy to write unit tests. Everyone wins, right?

Well, not everyone was drinking the Kool-Aid. I fielded a question about encapsulation that, at the time, I hadn’t prepared to answer. “Doesn’t this completely violate encapsulation?” I was a little poleaxed, and sputtered out an answer off the cuff, saying basically, “well, not completely….” I mean, if you’ve written a class that takes ILogger in its constructor and uses it for logging, I control the logger implementation but you control when and how it is used. So, you encapsulate the logger’s usage but not its implementation and this stands in contrast to what would happen if you instantiated your own logger or implemented it yourself — you would encapsulate everything and nothing would be up to me as a client of your code. Certainly, you have more encapsulation. So, I finished: “…not completely…. but who cares?!?” And that was the end of the discussion since we were out of time anyway.

koolaid

I was never really satisfied with that answer but, as Creedence Clearwater says, “time and tears went by, and I collected dust.” When I thought back to that conversation, I would think to myself that encapsulation was passe in the same way that deep inheritance hierarchies were passe. I mean, sure, I learned that encapsulation was one of the four cornerstone principles of OOP, but so is inheritance, and that’s kind of going away with “favor composition over inheritance.” So, hey, “favor dependency injection over encapsulation.” Right? Still, I didn’t find this entirely satisfying — just good enough not to really occupy much of a place in my mind.

But then I remember a bit of a brouhaha last year over a Stack Overflow question. The question itself wasn’t especially remarkable (and was relatively quickly closed), but compiler author and programming legend Eric Lippert dropped by to say “DI is basically a bad idea.” To elaborate, he said:

There is no killer argument for DI because DI is basically a bad idea. The idea of DI is that you take what ought to be implementation details of a class and then allow the user of the class to determine those implementation details. This means that the author of the class no longer has control over the correctness or performance or reliability of the class; that control is put into the hands of the caller, who does not know enough about the internal implementation details of the class to make a good choice.

I was floored. Here we have one of the key authors of the C# compiler saying that the “D” in the SOLID principles was a “bad idea.” I would have dismissed it as blasphemy if (1) I were the sort to adopt approaches based on dogma and (2) he hadn’t helped author at least 4 more compilers than I have. And, while I didn’t suddenly rip the IoC containers out of my projects and instantiate everything inside constructors, I did revisit this topic in terms of my thoughts.

Maybe encapsulation, in the information hiding sense, isn’t so passe. And maybe DI isn’t a magic bullet. But why not? What’s wrong with the author of a class ceding control over some aspects of its behavior by allowing collaboration? And, isn’t any method parameter technically a form of DI, if we’re going to be pedantic about it?

The more I thought about it, the more I started to see competing and interesting use cases. Or, I should say, the more I started to think what class authors are telling their collaborators by using each of these techniques:

Encapsulation: “Don’t worry — I got this.”
Dependency Injection: “Don’t worry — if this doesn’t work, you can always change it.”

So, let’s say that you’re writing a Mars Rover or maybe a compiler or something. The attitude that you’re going to bring to that project is one in which correctness, performance and reliability are all incredibly important because you have to get it right and there’s little room for error. As such, you’re likely going to adopt implementation preference of “I’m going to make absolutely sure that nothing can go wrong with my code.”

But let’s say you’re writing a line of business app for Initrode Inc and the main project stakeholder is fickle, scatterbrained, and indecisive. Then you’re going to have an attitude in which ease and rapidity of system changes is incredibly important because you have to change it fast. As such, you’re likely to adopt an implementation preference of “I’m going to make absolutely sure that changing this without blowing everything up is easy.”

There’s bound to be somewhat of an inverse relationship between flexibility and correctness. As a classic example, a common criticism of Apple’s “walled garden” approach was that it was so rigid, while a common praise of the same was how well it worked. So I guess my take-away from this is that Dependency Injection and, more broadly, Inversion of Control, is not automatically desirable, but I also don’t think I can get to Eric’s take that it’s “basically a bad idea,” either. It’s simply an exchange of “more likely to be correct now” for “more likely to be correct later.” And in the generally agile world in which I live and with the kind of applications that I write, “later” tends to give more value.

Uncle Bob Martin once said, I believe in his Clean Coders video series, that (paraphrased) the second most important characteristic of good software is that it meet the customer requirements. The most important characteristic is that it be easy to change. Reason being, if a system is correct today but rigid, it will be wrong tomorrow when the customer wants changes. If the system is wrong today but flexible, it’s easy to make it right tomorrow. It may not be perfect, but I like DI because I need to be right tomorrow.

23 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Rich
Rich
9 years ago

A good, thought provoking read. Thanks.

Erik Dietrich
9 years ago
Reply to  Rich

Thanks! Glad you liked it.

Medo
Medo
9 years ago

I think the type of relationship between the class and the dependency should be a main factor in deciding whether to inject it. A good measure is probably how the dependency affects testability. A “pure” dependency that doesn’t affect the outside world – e.g. a data structure – should most likely be a hidden detail, unless you really want to make it configurable to change the performance characteristics. Anything that the class uses to communicate with its environment – e.g. database connections or streams or objects that are shared between the class and the rest of the system – should… Read more »

Erik Dietrich
9 years ago
Reply to  Medo

I think you’ve defined some really nice heuristics here for when to invert control and when not to, frankly. I’ve never given it a ton of thought and have always favored erring on the side of injection for the sake of testability, with a loose governing principle that “coordinating” type classes (data access, repositories, controllers, etc) should be injected and “entity” type classes are fine to instantiate. But considering the external (to the application or module) interactions is probably a more “pure” way of reasoning about it, for lack of a better word. Good thoughts.

Tom Butler
9 years ago
Reply to  Medo

I agree with you but I think caution is in order if reuse is ever an issue. Let’s say you have an object (A) instantiating another object (B). You move these classes to another project where the requirements are slightly different. You have to change (B)’s constructor or replace (B) with (C) for the new project. This causes an issue because any enhancements/bugfixes to the (A) class cannot be easily backported to the original project as you have multiple branches.

Matt B
Matt B
9 years ago

Nice article and certainly made me think. The point about the Mars rover and making sure the code is correct could be taken two ways I suppose. In order to ensure your code works as expected it is important to write unit tests for it, obviously DI helps in this situation but of course you then have to hand over control of the dependencies over to something else. (p.s. I found a typo, excahnge should be exchange I’m guessing)?

Rob
Rob
9 years ago
Reply to  Matt B

In SOLID, the D is related to the S. If something is the responsibility of the class in question then I should be encapsulated, If the class needs something that is not its direct responsibility, such as a logging component, then that should be injected. It seems simple to me…

Matt
Matt
9 years ago
Reply to  Rob

I’m with you Rob. For the types of services and applications I work on, it’s usually pretty clear what should and shouldn’t be injected into a class, and I strongly disagree that DI violates encapsulation when done right (you inject *dependencies*, not *responsibilities*).
That said, I don’t write a lot of frameworks, work on OS kernels, device drivers, etc., so it may be that certain practicalities might favour a different approach for different kinds of software.

Erik Dietrich
9 years ago
Reply to  Matt B

Thanks for the heads up on the typo. Fixed now. First off, let me say that I’m a fan of DI and testing code units in isolation. That said, the Devil’s Advocate argument is that it’s not really important to *unit* test the code to ensure that it works, but rather to somehow verify (and probably automated) correctness checks. In other words, if you had monolithic code with few or no entry points, but you wrote exhaustive integration tests that covered every scenario and line of code, this would suffice for verification. The drawback would be extreme rigidity if requirements… Read more »

trailmax
9 years ago

Certainly an interesting thought to a bunch of cool-aid drinkers (such as myself -). What I don’t get is what is the opposite of DI. If we do not pass control from our class to (say) Logger, how do we do logging?
If our class creates an instance of Logger, that is not IoC in my mind, but we still pass control to some other component to to logging and that breaks encapsulation in some way.

Opposite of this I only see spaghetti code and ten-thousand-lines classes.

Mind… blown!

Erik Dietrich
9 years ago
Reply to  trailmax

A long time ago, I wrote a post advocating that people who had come from the C world to Java, C++ or C# should move to inverting control in their approach to OOP. ( https://daedtech.com/inverting-control ) In this post, I described the opposite of DI as “Command and Control.” In essence, what you have is main as the root of a tree structure, with the leaf nodes as the peons, like a company’s org chart. DI, on the other hand, strikes me as a paradigm more akin to building things with Legos — you assemble small pieces into slightly larger… Read more »

trailmax
9 years ago
Reply to  Erik Dietrich

Ah, that makes sense! thanks for the write-up!

Mihail Slavchev
9 years ago

I follow a simple rule for DI – I use it whenever my class has to use some orthogonal functionality (e.g. Logger). There are patterns which are more suitable than DI when my class has to change its behavior based on the injection.

Erik Dietrich
9 years ago

If you’re so inclined, do you have an example (gist or description)?

Mihail Slavchev
9 years ago
Reply to  Erik Dietrich

Think of it as of a rule of thumb, not a dogmatic law. Just sharing what works for me so far.

Erik Dietrich
9 years ago

I’m definitely a fan of rules of thumb over dogma 🙂

I was just genuinely curious as to an example or two of what you meant (e.g. do you modify behavior through inheritance, runtime binding, etc)?

Mihail Slavchev
9 years ago
Reply to  Erik Dietrich

It depends. When I think of traditional design patterns like state or strategy I see a beautiful usage of both composition and inheritance. My point is it is not one or another. I remember a recent case when I replaced DI with simple inheritance and it worked well for me. I was using DI to inject a factory which was responsible for returning the correct strategy depending on the available device memory. Take a look at this gist https://gist.github.com/anonymous/cee8ab7ddf64a2893cfe What I dislike in this DI option was that an important decision was hidden deep in the code. It was too… Read more »

Erik Dietrich
9 years ago

Thanks for the example. That makes a lot of sense now that I can visualize it, and I agree that your refactoring is an improvement. I like DI when it’s “flat,” so to speak. But I cringe when I see code bases that inject a dependency and the injected class queries the dependency for another and a third dependency. I find that unit tests help kind of bring these smells to the surface (I blogged about it once: https://daedtech.com/recognizing-test-smells ). You find yourself setting up mocks that return other mocks that return other mocks… and, yeah, that’s ugly. Your comment… Read more »

trackback

[…] Encapsulation vs Inversion of Control […]

trackback

[…] >> Encapsulation vs Inversion of Control […]

Tom Butler
9 years ago

Interesting article but the problem I have with it is you’re inferring that encapsulating all dependencies can somehow give you a different end product than injecting them… which is clearly not the case. You can build the exact same object graph with DI as you can using the new keyword in the constructor giving an identical level of ‘correctness’. The problem is, it’s not so strictly enforced and can be built in a different way, but by the same token, anyone who can alter new A(new B()); to new A(new C()) can also open up the A class and alter… Read more »

Erik Dietrich
9 years ago
Reply to  Tom Butler

If memory serves, I was writing the post with the perspective that I’d be writing a class and would have control over the class’s collaborators, but not how the class was used. So, if I were building a Restaurant class, and I wanted the restaurant to be cafeteria-style, I can control that. I could instantiate a CafeteriaStyle strategy object (or whatever) and enforce that — anyone who used my Restaurant would get it cafeteria style. But if I allow collaborators to inject an IStyle interface, I lose control of that. So now, this unknown user of my class has the… Read more »

Erik Dietrich
9 years ago
Reply to  Tom Butler

Interesting take on constructor instantiation and encapsulation, by the way. I’ve never thought of it that way before, and what you say makes sense to me.