Tuesday, September 23, 2014

Beyond SOLID: The Dependency Elimination Principle

Last post I explained why I don't teach the SOLID design principles.  Read the post for more detail, but the primary reason is that SOLID encourages heavy use of dependencies.  Applying SOLID to a codebase for even a short time will yield dependencies on abstractions everywhere -- quickly producing a codebase that is unintelligible.

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

Friday, August 22, 2014

Why I Don't Teach SOLID

If you talk about software design with anyone who cares about code quality, you won't spend much time before you hear SOLID.  SOLID is a mnemonic device that helps developers remember five important principles of object oriented design:

  • Single Responsibility Principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle
SOLID is helpful.  It came from really important people in our field.  It helps people reason about design.  It helps people make systems resilient to change. 

SOLID used to be a cornerstone of my design toolbox.  I used to strive to make my code SOLID.  I used to teach others to do the same.

Today, SOLID remains important, but I don't strive to make my code SOLID.  I rarely mention it when talking about design.  And I definitely don't teach it to developers eager to learn good design techniques.  It doesn't come with me in my toolbox.  It stays in a dusty box up in the attic.  I keep it because it is important, but I access it rarely.

SOLID causes problems.  Problems big enough that warrant stowing it away in the attic.  When I used to champion SOLID, those who resisted would point to these problems.  I would dismiss these people as naysayers, resistant to change, or disinterested in code quality.  But now I've learned that they were right all along. 

If I could use one word to represent these problems, it would be unintelligible.  Developers (myself included) applying SOLID frequently produce codebases that are unintelligible.  These SOLID codebases have low coupling.  And they are testable.  But they are unintelligible.  And often not as adaptable as the developers had hoped.

The main culprit is SOLID's focus on dependencies.  Open/Closed, Interface Segregation, and Dependency Inversion each encourage heavy use of dependencies defined as abstractions (i.e. a C#/Java interface or Abstract Class).  Open/Closed encourages abstractions for easy extensibility.  Interface Segregation encourages more client-defined abstractions.  Dependency Inversion says to depend on abstractions instead of concretions. 

These all lead developers to create interfaces everywhere.  They litter the codebase with interfaces like IFooer, IDoer, IMooer, and IPooer.  Navigating it becomes a nightmare.  You rarely know by inspection what piece of code is actually going to run.  But it's ok.  Because it's SOLID.  It's great design!

To help manage the madness, we then introduce an IoC container.  And a mocking framework for our tests.  If it was intelligible before, now it's really not intelligible.  You now literally can't find a call to 'new' anywhere in the code.  Good luck reasoning about any piece of code now.  But it's ok.  Because it's SOLID.  It's great design!

Is it great design if it's unintelligible?  Is it great design if a developer can't easily reason about it?  I don't think so.  SOLID is helpful and important.  But I don't think developers handle it well.  Which is why I don't teach SOLID anymore. 

Stay tuned for how to achieve what you're trying to gain with SOLID without the legibility baggage.

UPDATE: You can find the next part here.

Thursday, August 21, 2014

Here we go!

I decided to start a coding blog.  I've titled it Quality Is Speed because I don't believe the common anecdote that you have a tradeoff between quality and speed.  I believe the opposite is true: high quality is what enables speed.

Who knows if I'll consistently post here, but we'll give it a shot!