0

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.

7
  • 2
    "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?" - I am convinced the answer is "yes". DI is often just used for testing purposes, but not by end users . So now please tell us which part of your question is still unanswered.
    – Doc Brown
    Oct 8, 2023 at 19:39
  • 2
    "discussions online about how [DI] breaks encapsulation" - DI doesn't break encapsulation, for the same reason function parameters (or, better yet, higher order functions) don't break encapsulation. Encapsulation is not "here's a magical black box that swallows a call and hopefully does something as a side effect", but rather "here's closed box with slots designed for other boxes - a well defined interface with a well defined behavioral contract". What breaks encapsulation is the failure of the developer to define that contract (and thus what the internals even are) in the first place. Oct 9, 2023 at 1:07
  • @FilipMilovanović - Perhaps this is what you were trying to convey, but to me, "broken encapsulation" happens when you're careless about what you expose publicly. Even if you have an explicit contract set up, encapsulation would still be broken if you didn't design that contract in an intentional and thoughtful way. Behaviors such as "automatically adding a getter function for every private fields", or, "automatically exposing each class's dependencies publically" would both fall into this category for me. If you're intentional about the dependencies you expose, encapsulation won't be broken. Oct 9, 2023 at 4:33
  • I see two different philosophies around DI in comments and answers here. There's "DI" as in "you pass an instance of a class you own into someone else's class to configure its behavior" - which does not break encapsulation nor do I have any concerns about this. Then there "DI" as Martin Fowler defined as being a better name for the patterns IOC containers follow, and since the nature of IOC containers seems to be to encourage you to invert dependencies on nearly all of your classes "just in case", encapsulation seems to be a bit weaker (which isn't inherently a bad thing, it's a trade-off). Oct 9, 2023 at 5:10
  • Sometimes you just have to version the interface. That's why microsoft has eight different versions of IHTMLDocument learn.microsoft.com/en-us/previous-versions/windows/…
    – pjc50
    Oct 9, 2023 at 8:51

3 Answers 3

2

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.

2

I'm confused by two things.

  1. Why you think breaking changes are the same as breaking encapsulation?
  2. Why you haven't used DI to replace dependencies in other people's code as a regular thing?

Seriously have you never done something like:

class MyClient
{
   public MyClient(HttpClient client)
}

var x = new MyClient( //made by me
    new HttpClient( //made by MS
       new SpecialAuthHttpHandler("username","password") //made by SSO auth provider           )
    );

Or

Dictionary<string,IPriceCalc> myPriceCalculators..  //written by a bunch of people over the years and always changing

foreach(var item in basket)
{
   price += myPriceCalculators[item.type].GetPrice(item);
}

DI increases encapsulation and if you are constantly requiring changes to your interfaces you are doing something wrong. IFileReader is always going to be reading files, ITokeniser is always going to be generating collections of tokens?

6
  • Doesn't my example in my question constitute a breaking change for ITokenizer? I used to only need to peek at the next token, but my language has changed and now I need to be able to peek two tokens ahead. I could do some convoluted work to wrap ITokenizer in another class that buffers an extra token to allow me get this "peek 2 ahead" behavior without breaking existing code, but that's quite a lot of extra work to support an extendable point that may not even be used in the wild. (Because, again, how would one reasonably swap out a language's tokenizer). Oct 8, 2023 at 22:57
  • the question is why do you have peek() instead of tokeniser.GetTokens(string) or tokeniser.Next() the idea is to write fully functional components, not partial ones that only have the specific functionality that you need. Sure it comes up and then you have a breaking change. no big deal. But its unusual
    – Ewan
    Oct 9, 2023 at 10:09
  • So basically, dependency inversion should be done on any internal interface which I know to be fairly stable? But not on anything else? In this example, I would then need to write a wrapper class to go around this Tokenizer to handle the caching of the next token, so I can have the peek functionality that it's missing - and I assume I don't allow that class to be swapped out, because it's API is less stable? And then I should let outside users swap out implementations for my stable components, even if I don't have a concrete idea of why they would want to? Oct 9, 2023 at 13:50
  • No. You are advancing a strawman argument. The problem you describe, 1. isn't encapsulation breaking. 2. doesn't happen with good, or even 'normal' design.
    – Ewan
    Oct 9, 2023 at 14:43
  • Honestly, if the answer to my last comment was "yes", I would be fine with that - I didn't convey it clearly, but it wasn't meant to be an attack-type argument, Just trying to figure out your stance. Can you expound on how my previous comment is wrong? Is it 1. the thought that DI should only be done on stable APIs? (Are you trying to say that with good design, all classes should have a fairly stable API?) or 2. the fact that you should use DI on a class even when you don't why it would need to be swapped? (e.g. do you see value in swapping out a tokenizer?) Oct 9, 2023 at 15:50
1

Large scale use of DI is normally only seen in internal projects and generally for testing purposes. It does have some other isolated use cases, like supporting end user configurations.

Libraries used by clients may have some degree of DI but that is usually confined to IoC scenarios and with a strongly defined interface, that almost never changes. And in those cases, it almost never breaks encapsulation because you expect the interface and the library to behave the way it does.

Take a look at this class

https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.gzipstream?view=net-7.0

It takes a Stream as dependency. You can implement Stream in any way you want. Its interface is unlikely to ever change. This is a reasonable use of DI in a client library.

2
  • So, in other words, "apply DI, just in case, on nearly all of your classes" is a fine philosophy to follow when we're discussing internal projects, but becomes bad if we need to allow our consumers to inject dependencies into our classes. Instead, we should be much more intentional about the interfaces we give them and what we allow them to customize? I can get behind that. Oct 8, 2023 at 23:20
  • 1
    It depends on the internal project. Too much DI becomes unwieldy without an IOC container and if you go really overboard, it's a maintenance hurdle.
    – Ccm
    Oct 9, 2023 at 6:44

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.