43

I am currently trying to figure out SOLID. So the Dependency Inversion Principle means that any two classes should communicate via interfaces, not directly. Example: If class A has a method, that expects a pointer to an object of type class B, then this method should actually expect an object of type abstract base class of B. This is helps for Open/Close as well.

Provided that I understood that correctly, my question would be is it a good practice to apply this to all class interactions or should I try to think in terms of layers?

The reason I am skeptical is because we are paying some price for following this principle. Say, I need to implement feature Z. After analysis, I conclude that feature Z consists of functionality A, B and C. I create a facade class Z, that, through interfaces, uses classes A, B and C. I begin coding the implementation and at some point I realize that task Z actually consists of functionality A, B and D. Now I need to scrap the C interface, the C class prototype and write separate D interface and class. Without interfaces, only the class would've needed to be replaced.

In other words, to change something, I need to change 1. the caller 2. the interface 3. the declaration 4. the implementation. In a python directly coupled implementation, I would need to change only the implementation.

  • 12
    Dependency inversion is simply a technique, so it should only be applied when needed... there is no limit to the degree to which it can be applied, so if you apply it everywhere, you end up with garbage: as with every other situation-specific technique. – Frank Hileman Feb 25 '15 at 22:53
  • To put it simply, the application of some software design principles depend on being able to refactor mercilessly when requirements are changing. Of those, the interface part is believed to best capture the contractual invariants of the design, whereas the source code (implementation) is expected to tolerate more frequent changes. – rwong Feb 26 '15 at 5:58
  • @rwong An interface captures contractual invariants only if you use a language that supporting contractual invariants. In common languages (Java, C#), an interface is simply a set of API signatures. Adding superfluous interfaces only degrades a design. – Frank Hileman Feb 26 '15 at 17:25
  • I would say you understood it wrong. DIP is about avoiding compile-time dependencies from a "high-level" component to a "low-level" one, in order to allow reuse of the high-level component in other contexts, where you would use a different implementation for the low-level component; this is done by creating an abstract type at the high-level, which is implemented by the low-level components; so both high- and low-level components depend on this abstraction. In the end, high and low level components do communicate through an interface, but this is not the essence of DIP. – Rogério Feb 26 '15 at 21:37
86

In many cartoons or other media, the forces of good and evil are often illustrated by an angel and a demon sitting on the character's shoulders. In our story here, instead of good and evil, we have SOLID on one shoulder, and YAGNI (You ain't gonna need it!) sitting on the other.

SOLID principles taken to the max are best suited for huge, complex, ultra-configurable enterprisey systems. For smaller, or more specific systems, it is not appropriate to make everything ridiculously flexible, as the time you spend abstracting things will not prove to be a benefit.

Passing interfaces instead of concrete classes sometimes means for example that you can easily swap reading from a file for a a network stream. However, for a great amount of software projects, that kind of flexibility is just not ever going to be needed, and you might as well just pass concrete file classes and call it a day and spare your brain cells.

Part of the art of software development is having a good sense of what is likely to change as time goes on, and what isn't. For the stuff that is likely to change, use the interfaces and other SOLID concepts. For the stuff that won't, use YAGNI and just pass concrete types, forget the factory classes, forget all the runtime hooking up and configuration, etc, and forget a lot of the SOLID abstractions. In my experience, the YAGNI approach has proven to be correct far more often than it is not.

  • 19
    My first introduction to SOLID was about a 15 years ago on a new system we were building. We all drank the kool aid man. If anybody mentioned anything that sounded like YAGNI, we were like "Pfffft... plebeian". I had the honor (horror?) of watching that system evolve over the course of the next decade. It became an unwieldy mess that no one could understand, not even we founders. Architects love SOLID. People who actually earn their living love YAGNI. Neither is perfect, but YAGNI's closer to perfect, and should be your default if you don't know what you're doing. :-) – Calphool Feb 25 '15 at 21:09
  • 11
    @NWard Yeah, we did that on a project. Went nuts with it. Now our tests are impossible to read or maintain, in part because of over-mocking. On top of that, because of the dependency injection, it's a pain in the rear to navigate through the code when you're trying to figure something out. SOLID isn't a silver bullet. YAGNI isn't a silver bullet. Automated testing isn't a silver bullet. Nothing can save you from doing the hard work of thinking about what you're doing and making decisions about whether it's going to help or hinder your work or someone else's. – jpmc26 Feb 25 '15 at 23:26
  • 22
    A lot of anti-SOLID sentiment here. SOLID and YAGNI are not two ends of a spectrum. They're like the X and Y coordinates on a chart. A good system has very little superfluous code (YAGNI) AND follows the SOLID principles. – Stephen Feb 26 '15 at 0:38
  • 31
    Meh, (a) I disagree that SOLID = enterprisey and (b) the whole point of SOLID is that we tend to be extremely poor forecasters of what will be needed. I have to agree with @Stephen here. YAGNI says that we should not try to anticipate future requirements that aren't clearly spelled out today. SOLID says that we should expect the design to evolve over time and apply certain simple techniques to facilitate it. They are not mutually exclusive; both are techniques for adapting to changing requirements. The real problems happen when you try to design for unclear or very distant requirements. – Aaronaught Feb 26 '15 at 4:53
  • 8
    "you can easily swap reading from a file for a a network stream" -- this is a good example where an over-simplified description of DI leads people astray. People sometimes think (in effect), "this method will use a File, so instead it'll take an IFile, job done". Then they can't easily substitute a network stream, because they over-demanded the interface, and there are operations in IFile the method doesn't even use, that don't apply to sockets, so a socket can't implement IFile. One of the things DI isn't a silver bullet for, is inventing the right abstractions (interfaces) :-) – Steve Jessop Feb 26 '15 at 10:29
11

In layman's words:

Applying the DIP is both easy and fun. Not getting the design right at the first attempt is not enough reason giving up on the DIP altogether.

  • Usually IDEs help you do that kind of refactoring, some even allow you to extract the interface out of an already implemented class
  • It's almost impossible to get the design right the first time
  • The normal workflow involves changing and rethinking the interfaces in the first stages of developement
  • As development evolves it matures and you will have less reasons to modify the interfaces
  • In an advanced stage the interfaces (the design) will be mature and will hardly change
  • From that moment on, you begin to reap the benefits, since your app is open to scale up.

In the other hand, programming with interfaces and OOD can bring the joy back to the sometimes stale craft of programming.

Some people say it adds complexity but I think the opossite is true. Even for small projects. It makes testing/mocking easier. It makes your code to have fewer if any case statements or nested ifs. It reduces cyclomatic complexity and makes you think in fresh ways. It makes programming more alike to real-world design and manufacturing.

  • 5
    I don't know what languages or IDE's are being used by the OP, but in VS 2013 it is ridiculously simple to work against interfaces, extract interfaces and implement them, and critical if TDD is used. There is no extra development overhead for developing using these principles. – stephenbayer Feb 25 '15 at 23:15
  • Why is this answer talking about DI if the question was about DIP? DIP is a concept from the 1990's, while DI is from 2004. They are very different. – Rogério Feb 26 '15 at 21:59
  • 1
    (My previous comment was meant for another answer; ignore it.) "Programming to interfaces" is much more general than DIP, but it's not about making every class implement a separate interface. And it only makes "testing/mocking" easier if the testing/mocking tools suffer from serious limitations. – Rogério Feb 26 '15 at 22:07
  • @Rogério Usually when using DI, not every class implements a separate interface. One interface being implemented by several classes is common. – Tulains Córdova Feb 26 '15 at 23:32
  • @Rogério I corrected my answer, every time I mentioned DI I meant DIP. – Tulains Córdova Nov 3 '16 at 11:53
9

Use dependency inversion where it makes sense.

One extreme counterexample is the "string" class included in many languages. It represents a primitive concept, essentially an array of characters. Assuming you could change this core class, it makes no sense to use DI here because you will never need to swap out the internal state with something else.

If you have a group of objects used internally in a module that are not exposed to other modules or reused anywhere, it is probably not worth the effort to use DI.

There are two places where DI should automatically be used in my opinion:

  1. In modules designed for extension. If the entire purpose of a module is to extend it and change behavior, it makes perfect sense to bake DI in from the start.

  2. In modules that you are refactoring for the purpose of code reuse. Perhaps you coded a class to do something, then realize later that with a refactor you can leverage that code elsewhere and there is a need to do so. That is a great candidate for DI and other extensibility changes.

The keys here are use it where it is needed because it will introduce extra complexity, and make sure you measure that need either through technical requirements (point one) or quantitative code review (point two).

DI is a great tool, but just like any* tool, it can be overused or misused.

* Exception to the above rule: a reciprocating saw is the perfect tool for any job. If it does not fix your problem, it will remove it. Permanently.

  • 3
    What if "your problem" is a hole in the wall? A saw would not remove it; it would make it worse. ;) – Mason Wheeler Feb 25 '15 at 19:02
  • 3
    @MasonWheeler with a powerful and fun to use saw, "hole in wall" could turn into "doorway" which is a useful asset :-) – user22815 Feb 25 '15 at 19:03
  • 1
    Can't you use a saw to make a patch for the hole? – JeffO Feb 25 '15 at 20:33
  • While there are some advantages to having a non-user-extensible String type, there are many cases where alternative representations would be helpful if the type had a good set of virtual operations (e.g. copy a substring to a specified portion of a short[], report whether a substring does or might contain only ASCII, try to copy a substring believed to contain only ASCII to a specified portion of a byte[], etc.) It's too bad frameworks don't have their string types implement any useful string-related interfaces. – supercat Feb 25 '15 at 23:05
  • 1
    Why is this answer talking about DI if the question was about DIP? DIP is a concept from the 1990's, while DI is from 2004. They are very different. – Rogério Feb 26 '15 at 22:01
5

It seems to me that the original question is missing part of the point of the DIP.

The reason I am skeptical is because we are paying some price for following this principle. Say, I need to implement feature Z. After analysis, I conclude that feature Z consists of functionality A, B and C. I create a fascade class Z, that, through interfaces, uses classes A, B and C. I begin coding the implementation and at some point I realize that task Z actually consists of functionality A, B and D. Now I need to scrap the C interface, the C class prototype and write separate D interface and class. Without interfaces, only the class would wave needed to be replaced.

To truly take advantage of the DIP, you would create class Z first, and have it call the functionality of classes A, B and C (which are not yet developed). This gives you the API for classes A, B and C. Then you go and create classes A, B and C and fill in the details. You effectively should be creating the abstractions you need as you're creating class Z, based entirely on what class Z needs. You can even write tests around class Z before classes A, B or C are even written.

Remember that the DIP says that "High-level modules should not depend on low-level modules. Both should depend on abstractions."

Once you have worked out what class Z needs and the way in which it wants to get what it needs, you can then fill in the details. Sure, sometimes changes will need to be made to class Z, but 99% of the time this won't be the case.

There will never be a class D because you worked out that Z needs A, B and C before they were written. A change in requirements is a different story altogether.

5

The short answer is "almost never", but there are, in fact, a few places where the DIP doesn't make sense:

  1. Factories or builders, whose job it is to create objects. These are essentially the "leaf nodes" in a system that fully embraces IoC. At some point, something has to actually create your objects, and can't depend on anything else to do that. In many languages, an IoC container can do it for you, but sometimes you need to do it the old-fashioned way.

  2. Implementations of data structures and algorithms. Generally, in these cases the salient characteristics that you're optimizing for (such as running time and asymptotic complexity) depend upon specific data types being used. If you're implementing a hash table, you really need to know that you're working with an array for storage, not a linked list, and only the table itself knows how to properly allocate the arrays. You also don't want to pass in a mutable array and have the caller break your hash table by fiddling with its contents.

  3. Domain model classes. These implement your business logic, and (most of the time) it only makes sense to have one implementation, because (most of the time) you're only developing the software for one business. Although some domain model classes might be constructed using other domain model classes, this is generally going to be on a case-by-case basis. Since domain model objects don't include any functionality that can be usefully mocked, there's no testability or maintainability benefit to the DIP.

  4. Any objects which are provided as an external API and need to create other objects, whose implementation details you don't wish to expose publicly. This falls under the general category of "library design is different from application design". A library or framework may make liberal use of DI internally, but will eventually have to do some actual work, otherwise it's not a very useful library. Let's say you're developing a networking library; you really do not want the consumer to be able to provide its own implementation of a socket. You might internally use an abstraction of a socket, but the API you expose to callers is going to create its own sockets.

  5. Unit tests, and test doubles. Fakes and stubs are supposed to do one thing and do it simply. If you have a fake that's complex enough to worry about whether or not to do dependency injection, then it's probably too complex (perhaps because it's implementing an interface that's also far too complex).

There might be more; these are the ones I see on a somewhat frequent basis.

  • How about "any time you're working in a dynamic language"? – Kevin Feb 26 '15 at 17:00
  • No? I do plenty of work in JavaScript and it still applies equally well there. The "O" and "I" in SOLID can get a little blurry, though. – Aaronaught Feb 28 '15 at 5:45
  • Huh... I find that Python's first-class types combined with duck typing make it rather less necessary. – Kevin Feb 28 '15 at 14:49
  • The DIP has absolutely nothing whatsoever to do with the type system. And in what way are "first-class types" unique to python? When you want to test something in isolation, you're supposed to substitute test doubles for its dependencies. Those test doubles can be alternate implementations of an interface (in statically-typed languages) or they can be anonymous objects or alternate types that happen to have the same methods/functions on them (duck typing). In both cases, you still need a way to actually substitute an instance. – Aaronaught Feb 28 '15 at 16:27
  • 1
    @Kevin python was hardly the first language to possess either dynamic typing or loose mocks. It's also completely irrelevant. The question is not what an object's type is but how/where that object is created. When an object creates its own dependencies, you're forced into unit-testing what should be implementation details, by doing horrible things like stubbing out constructors of classes that the public API makes no mention of. And forgetting about testing, mixing behavior and object construction simply leads to tight coupling. Duck typing doesn't solve either of those problems. – Aaronaught Mar 1 '15 at 0:45
2

Some signs you may be applying DIP at too micro of a level, where it's not providing value:

  • you have a C/CImpl or IC/C pair, with only a single implementation of that interface
  • the signatures in your interface and implementation match one-to-one (violating DRY principle)
  • you frequently change C and CImpl at the same time.
  • C is internal to your project and not shared outside your project as a library.
  • you are frustrated by F3 in Eclipse/F12 in Visual Studio taking you to the interface instead of the actual class

If this is what you're seeing, you might be better off just having Z call C directly and skip the interface.

Also, I don't think of method decoration by a dependency injection / dynamic proxy framework (Spring, Java EE) the same way as true SOLID DIP - this is more like an implementation detail of how method decoration works in that tech stack. The Java EE community considers it an improvement that you don't need Foo/FooImpl pairs like you used to (reference). By contrast, Python supports function decoration as a first-class language feature.

See also this blog post.

0

If you always invert your dependencies, then all your dependencies are upside down. Which means that if you started with messy code with a knot of dependencies, that's what you still (really) have, just inverted. Which is where you get the problem that every change to an implementation needs to change its interface too.

The point of dependency inversion is you selectively invert the dependencies that are making things tangles. The ones that should go from A to B to C still do so, it the ones that were going from C to A that now go from A to C.

The result should be a dependency graph that is free of cycles - a DAG. There are various tools that will check this property, and draw the graph.

For a fuller explanation, see this article:

The essence of applying the Dependency Inversion Principle correctly is this:

Split the code/service/… you depend on into an interface and implementation. The interface restructures the dependency in the jargon of the code using it, the implementation implements it in terms of its underlying techniques.

The implementation remains where it is. But the interface has a different function now (and uses a different jargon/language), describing something the using code can do. Move it to that package. By not placing the interface and implementation in the same package, the (direction of the) dependency is inverted from user→implementation to implementation→user.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.