Dependency Injection – How to Reconcile Encapsulation Issues

architectural-patternsdependency-injectiondesign-patterns

I was reading Martin Fowlers take on Dependency Injection, and in general have been trying to discuss it a bit online to help get rid of my own misconceptions and to understand this principle better.

In this article, he talks about one of the benefits of using dependency injection, which is that it allows you to share your program to a friend, and then they can replace some of your classes with their own according to their needs. All of this allows your code to be more reusable and extensible. Sounds great!

So lets assume we actually did this – we've published our program online, and now there's a variety of people who are using it, but who have rewired up some of the dependencies however they want so they can inject custom behaviors into our program. And lets assume we're creating our very own scripting language. So we have a Language class that depends on classes like FileReader, Tokenizer, TokenStreamToAst, TypeChecker, and Runtime. Each of these classes in turn depends on a variety of other stuff.

Now, if understand correctly how DI gets used, it tends to get used in a sort-of "just-in-case" philosophy. i.e. I'm going to make it so all of those classes (FileReader, Tokenizer, etc) are passed directly into the constructor, allowing them to be swapped out with alternative implementations, not because I can currently see a concrete reason people may wish to swap these dependencies out, but because I know I can't see the future, and it's possible that one day, some requirement pops up, either for me or someone I'm sharing this with, and we'll need to swap the dependency out at that point.

Putting all of this together, we've now got a major problem, and it has to do with broken encapsulation. Lets say I want to add a new method to my tokenizer class – currently it has a peek() method to let me see the next token in line, and I want to add a peek2() method to see the token after that. Well, I can't without making it a breaking change – what if someone else has swapped out my tokenizer for their own – they wouldn't have a .peek2() method. In fact, generally speaking, since each of my classes has an interface with it, and since there's always a change that someone is implementing my interface, I can't ever add a new method to one of my classes without it being a breaking change. If I was following the semantic versioning guidelines, almost every feature addition would need to be counted as a "breaking change", and thus be a major version update!

So how do people reconcile the use of DI with the way it breaks encapsulation?

  • Perhaps those who rewire the dependencies are doing so at their own risk, understanding that upstream updates will constantly be breaking their version of the code? Not too different from the risks people take when they fork a project?
  • Maybe Martin Fowler was just spouting off the idea of letting others rewire your dependencies as a potential use-case for DI, but in practice people don't usually permit their program users to rewire their dependencies, and so don't have to worry about breaking end-users' code in this fashion? (Encapsulation is much more important when we're talking about a public contract we want to keep stable as opposed to an internal-only contract).
  • I don't understand this to be the case, but maybe Dependency Injection doesn't actually follow this "just-in-case" philosophy I was describing? e.g. in our above example, maybe we only allow the FileReader class to be swapped out by others, so they can run scripts from, say, over the internet? But we don't allow things like the Tokenizer to be swapped, since there's really no easy way to provide a custom Tokenizer? (I mean, imagine that the Python language was written with DI – how would one swap out its tokenizer and have the whole thing still work in a sensible way, unless they basically copy-paste the original Tokenizer implementation and tweak it slightly – at which point they might of well just forked the Python language instead).
  • Maybe there's something else going on and I'm completely misunderstanding how this all fits together?

I have seen past discussions online about how Dependency Injection breaks encapsulation, such as over here, but none of these previous discussions I've seen actually talks about the "encapsulation breaking" in conjunction with the fact that there seems to be this idea that we can share our code with others and let them rewire our dependencies. They're always seem to be discussed in the context that you're always going to be the one in charge of wiring up your own dependencies.

Best Answer

Maybe Martin Fowler was just spouting off the idea of letting others rewire your dependencies as a potential use-case for DI, but in practice people don't usually permit their program users to rewire their dependencies, and so don't have to worry about breaking end-users' code in this fashion?

Pretty much this - the vast majority of times I've ever used dependency injection is in "internal" code when I (possibly using "I" in the corporate sense) am in full control of what gets pushed in.

The few times I've exposed dependency injection as part of the public interface of any kind of library code it has been for the real cross-cutting concerns: logging, memory management (when I wrote C), etc. In those cases, yes, you do define an interface for the things that are being passed in, and yes, changing that interface is a breaking change. But an interface which is "allocate a chunk of memory" and "deallocate a chunk of memory" doesn't change very often.

Related Topic