In the last post I looked a bit at the Dependency Inversion Principle which says to define an interface representing the contract of a dependency that you need someone to fulfill for you. It's a really great technique for encouraging SRP and loose coupling.
The whole idea behind the DIP is that your class depends only on the interface, and doesn't know anything about the concrete class implementing that interface. It doesn't know what class it's using, nor does it know where that class came from. And thus, we gain what we were after: the high level class can be reused in nearly any context by providing it with different dependencies. And on top of that, we gain excellent separation of concerns, making our code base more flexible, more maintainable, and I'd argue more understandable. Clearly, the DIP is awesome!
But! I'm sure you were waiting for the But... Since we now have to provide our object with it's dependencies, we have to know:
- Every interface it depends on
- What instances we should provide
- How to create those instances
Fortunately, someone invented Inversion of Control Containers, so we don't have to create these objects ourselves. Or, maybe unfortunately, 'cause now we don't have to create these objects ourselves, which sweeps any unsavory design issues away where you wont see them...
What design issues? Well, the leaking of implementation details obviously! Are there others? Probably. Maybe class design issues with having too many dependencies? Or having dependencies that are too small and should be rolled into more abstract concepts? But neither of these is a result of injecting Inverted Interfaces, only the implementation leaking.
I do believe this is leaky, but I'm not really sure if it's a problem exactly. At the end of the day, we're really just designing a plugin system. We want code to be reusable, so we want to be able to dynamically specify what dependencies it uses. We're not forced to pass the dependencies in, we could use a service locator type approach. But this has downsides of it's own.
In the next post, I'll wrap this up by zooming back out and trying to tie this all together.
I can see how you came to these conclusions - and some of it may still be valid - but I think your understanding of DIP is off just a bit, and I think it's throwing you down this path for the wrong reasons.
ReplyDeleteBased on what you've written in this post, I think you've made the most common mistake people make with DIP (I misunderstood this for years): you've confused DIP with Dependency Abstraction and Dependency Injection. While DIP may lead to dependency abstraction and dependency injection in a language like c#, that is not it's purpose. That's only a side effect of implementing DIP in c#
start here
http://code-magazine.com/article.aspx?quickid=1001061&page=7 (halfway down the page)
and here
http://blog.rubybestpractices.com/posts/gregory/055-issue-23-solid-design.html#comment-317367342 (read my entire conversation w/ the author in these comments)
I agree with you that the problems you point out are real and are obnoxious problems. I don't think they are caused by DIP directly. They are caused by C# and our attempts at writing better structured software in C# are exposing the weaknesses and problems inherent in C#.
Hey Derick, thanks for the feedback. I'm sorry I was unclear in this post, I was trying to zoom into the leaky nature of Dependency Inversion which is frequently introduced in order to comply with DIP. I agree completely with what you're saying that DIP != DI, but DI is the technique frequently used to accomplish DIP in a static language like C#. So if you're not careful, like I clearly wasn't in this post, it can get blurred.
DeleteIn fact, if you read the previous post I talk about how the concept of "interface ownership" can get a bit confusing in top-down development (at the very end of the post).
I have some thoughts that I'm not directly talking about in these posts, but which I can't help hinting about, and your comment is kind of calling me out on that. I touch on it a bit more in the next post in this series. But as long as we're chatting, let me just call it out directly.
The first link you sent uses the common analogy of the light plugging into an electrical socket. The electrical socket is the interface. But who owns it? Which component is the "higher level:" the light, or the wiring? I think this concept of "interface ownership" is a bit of red herring. The purpose of DIP is to make your components reusable. DIP is a response to the common practice of components depending on the interfaces of "low level" components that just happened to exist. By focusing on what the component needs to accomplish instead of what interfaces happen to be laying around, you make it possible to plugin different dependencies. Thus you gain reuse, loose coupling => good design. That's the part that is important. Not which component is "higher level" than the other, or who owns the interface. As long as both the wiring and the light comply with the outlet interface, I'm a happy man. To me these types of plug-able interfaces are bigger than just a simple inversion or ownership.
The next step is to notice that the outlet is the only plug-able interface in the whole wiring -> light system. The light doesn't have an interface between the plug and the wire, the wire and the light fixture base, the fixture base and the fixture body, the fixture body and the light bulb connector. That would be totally excessive and too costly. The only plug-able interfaces required were from the wiring to the light, and the light to the light bulb.
But too often, we apply DIP between all of our layers and classes instead of identifying what needs to be plug-able. This is when the leakiness really becomes an issue. Sadly, it's often unit testing that forces us into this...
I understand that I'm splitting hairs with this. I'm basically saying the same thing DIP does. But I kind of think DIP made us plug-able crazy. And if we step back and focus on where we want re-use and loose coupling, instead of just inverting every dependency, we'd have simpler, more cohesive code, and easier to work with code.