What should we do about these dependencies? We start applying the Dependency Elimination Principle.
Treat Dependencies As Code Smells
With the Dependency Elimination Principle, we change our default to treat dependencies as code smells. This doesn't mean we won't have dependencies. It just means our default is to treat them as code smells. This new default forces us to examine why we need each dependency and how to eliminate each.
Are we getting something from the dependency? If so, pass the thing we're getting, not the thing that gives it to us.
Are we sending something to the dependency? If so, consider a model that uses events + event binding instead of passing in an interface. Or use a DTO that represents the state of the dependency and respond to changes to it (e.g. MVVM).
Whole Value
Most codebases have way too many dependencies because they don't have their concepts defined as Whole Values. Here are some simple pointers to create a clean, intelligible codebase that won't use a lot of dependencies on abstractions (lifted from J.B. Rainsberger's take on the 4 Rules of Simple Design):
• Kill Primitive Obsession
• Name things as Nouns (and not using verb nouns that have an ‘er’ ending)
• Remove duplication
Applying these simple pointers will shock you in how quickly your code becomes simple, readable, testable, and extendable but without all of the baggage that comes with littering interfaces everywhere.
Using Testability as the Lens
Let's look at the Dependency Elimination Principle from another lens: testability. Code with dependencies (even if those dependencies are mockable) is more difficult to test than code without them. Here are the levels as I think of them from easiest to hardest (I think I got this from another blog post but can't for the life of me find it):
- Level 0: A pure static function with no side effects
- Level 1: A class that has immutable state. Think a Whole Value that replaces a primitive, like EmailAddress or PhoneNumber.
- Level 2: A class that has mutable state and may operate against behaviorless dependencies like Level 1 Whole Values.
- Level 3: A class that operates against a dependency with its own behaviors
Level 0 is trivial to test. Just pass in the different inputs and expect the right outputs.
Level 1 is not much different than level 0, except you have a few more methods to test and a few more configurations to test as well. Level 1 is nice because you get to encapsulate a concept into a Whole Value.
Level 2 is more difficult than level 1 because you have to manage the internal state and test different cases when the state changes. But sometimes you’ll want level 2 code because of the benefits it brings in encapsulating a concept into a Whole Value.
Level 3 is the most difficult to test. You either use mocks or you test more than one thing at once.
I want to make testing as easy as possible, so I strive to choose the lowest level that meets my needs. This means lots of Level 0 and Level 1 code. Some Level 2 and rarely any Level 3. My code becomes mostly functional but takes advantage of OO to create nice Whole Values that keep all related behavior in the same place.
Circling Back to SOLID
Assume we've applied the DEP to a codebase. Let's analyze how SOLID that codebase is:
• Single Responsibility Principle: Heck yes. Whole Values everywhere. Extremely high cohesion.
• Open-Closed Principle: Yes, but in a different way. The openness is in the way we compose all the little Whole Values, not in plugging in new dependencies everywhere.
• Liskov Substitution Principle: Irrelevant. We're not using inheritance much anymore.
• Interface Segregation Principle: Again, irrelevant. We aren’t using many interfaces
• Dependency Inversion Principle: Mostly irrelevant because most dependencies are eliminated. The dependencies that remain are just Whole Values which you treat as part of your type system and perhaps a tiny handful of interfaces to talk to the outside world.
It's all about the Single Reponsibility Principle
Applying the Dependency Elimination Principle leaves you laser-focusing on the Single Responsibility Principle. And you get the flexibility of Open-Closed that leads to business agility. And you lose all the legibility baggage that comes with Liskov, ISP, and DIP. Wins all around.
For more reading on Dependency Elimination, check out Arlo Belshee's posts about No Mocks. He's the one who introduced all of these ideas to me.
(UPDATE: It's possible the unknown source of the levels described above is from John Sonmez's blog here and in related posts)
(UPDATE 2: Check out an example that shows a common class of unnecessary dependencies here)
(UPDATE 3: Another example that shows how Primitive Obsession leads to unnecessary dependencies