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
Great post! The levels you mention seem a lot like this post from John Sonmez http://simpleprogrammer.com/2011/01/14/back-to-basics-unit-testing-automated-blackbox-testing-and-conclusions/
ReplyDeleteThanks! Perhaps that's where I saw them in the past. I will link to these in the post
DeleteThis sounds similar to what I wrote recently as Beyond Mock Objects, which really amounts to Beyond Depending on Interfaces.
ReplyDeleteIn essence, I prefer depending on data over depending on the source of the data (usually an interface). I use the example of depending on a timestamp instead of a Clock interface. It sounds like the typical unnecessary dependency to eliminate.
Some code, however, seems to need to fire an event or cause an action. Do you believe that this is not the case? or merely that we write too much code that fires actions when it could really just depend on data? or something else?
Also, example plz. :)
I read more closely. It looks like you've answered my question in the text. Sorry about that. Pairing on my training problem with this style could prove illuminating. Let me know if you're up for that.
ReplyDeleteThat sounds fun to pair on this. Let's set something up. You can reach me at first dot last at gmail.
DeleteThis comment has been removed by the author.
ReplyDeleteSo take the example of a web form into which an address is typed. Then the address is sent to an address validation service to get an OK (or not) before processing further. At first glance this appears to me to be irreducibly level 3. But you're suggesting wrapping the service in a DTO with events? Or have I misunderstood?
ReplyDeleteThanks
Chris, you can go a few different directions here. The key is that your code doesn't actually care about the validation service. It just cares about the answer it got.
DeleteSo wherever the code is using the service to validate addresses, instead just pass the validation result or the Address instance or whatever you actually got from the service. If you need to worry about async, you can wrap that in a Task (C#) or something similar that represents a Future.
Then you can create a one-liner that instead of passing the parameter, it makes the call to the service. I don't test that one-liner in my unit test suite. It's so trivial and changed so infrequently that I just let it go.
Let me know if you want to see a real code sample. Also if you have code that's doing this you're willing so share, we can work in that too.
Hey, dude. So, this mindset is just functional programming :-). C# has some niceties over java like value objects for this.
ReplyDeleteIf you haven't seen them already, Rich Hickey covers it in all of this talks, specifically the concepts of 'design by decoupling', immutability, 'data'-oriented thinking, etc.
Nice @gtrak. This was a good post talking about SOLID & functional programming:
Deletehttp://blog.ploeh.dk/2014/03/10/solid-the-next-step-is-functional/
Brian, I am interested in using Whole Values, but I don't want the overhead of full-on DDD and I feel like that's where this leads. Am I wrong?
ReplyDeleteWhat J.B. said :)
DeleteAnd it's important to hold design frameworks along with Simple Design, Emergent Design, and YAGNI. The key is to refactor your code into the shape it needs to be at the current moment -- nothing more, nothing less.
DeleteAutomated refactoring tools like ReSharper in .NET make this "just-in-time" refactoring low cost. And a lot of fun.
Indeed so, @gtrak. I have noticed that as I practise more TDD and I refine my "sense of smell" it pushes me in the direction of fewer and fewer dependencies on only pure actions (functions with side-effects and no return value). It feels very functional.
ReplyDelete@Brian MacKay, there's no law that requires you to drag Eric Evans' book into a code that uses Whole Values. He certainly didn't invent them. For example: http://c2.com/ppr/checks.html
ReplyDeleteCan you provide an example for the typical MVC/Repository pattern that is common today? You know, there's a ProductsController with a dependency on IProductRepository. And actions /Show/Id and /Delete/Id delegate to the repository's Fetch(primitive Id) and Delete(primitive Id) methods.
ReplyDeleteI'm having trouble mapping that sort of dance into the ideas you've mentioned in this post. Something's gotta take that Id from the web request and translate it into a Product. Is the controller the wrong place to be doing that? Seems like we're just pushing dependencies around? Maybe a controller is a bad example?
I think much of the accidental complexity comes from not so powerful mocking tools like Mockito. You're forced to use interface or make methods non-final and to avoid static methods. With powerful mocking framework like JMockit all of this restrictions are gone and the design can be simplified without affecting testability. http://jmockit.github.io/
ReplyDeleteThanks for a great article!
ReplyDeleteI know that it has been a while since you published the article but I hope you still have time for a question. You write: "Are we getting something from the dependency? If so, pass the thing we're getting, not the thing that gives it to us.". That is fine if the method we call doesn't take any parameters. But what if the method takes parameters and the value of these parameters are determined by the logic inside the method that calls the dependency?
Thanks in advance,
Anders
Anders Baumann, maybe you must split your method (with dependencies) into parts, to follow SRP. Call new method to determine parameters, call dependency, pass results to original method.
ReplyDelete