SOLID Design Principles - Dependency Injection

Not as Painful as it sounds…

Nothing is more pleasing than beautiful code. And nothing is more heart-breaking than watching beautiful code get destroyed.

Lately, I’ve been paying particular attention to SOLID Object Oriented Design (OOD) principles and their interaction with TDD. I’m finding that, while TDD is an essential first step, it just isn’t enough. If I want my code to survive the rigors of change and be useful for a long time I need to armor it by following SOLID principles.

There’s a delightful symmetry in the feedback loop between TDD and OOD. TDD in isolation is not guaranteed to produce well designed code, but if you follow the OOD principles while writing the code you’re testing, TDD gets easier and your tests get better.

In case you feel like pushing back already, let me add an early caveat. If you

  • have an extremely simple application
  • with a specification that’s 100% complete
  • that will never ever change

go on ahead and write it any way you’d like. It doesn't matter.

I say good luck with that. Let me know how it works out for you.

But if you’re living in my reality, have a listen to Uncle Bob. In Design Principles and Design Patterns he describes good software as "clean, elegant, and compelling", with "a simple beauty that makes the designers and implementers itch to see it working".

But then goes on to say:

"What goes wrong with software? The software starts to rot. At first it isn’t so bad. An ugly wart here, a clumsy hack there, but the beauty of the design still shows through. Yet, over time as the rotting continues, the ugly festering sores and boils accumulate until they dominate the design of the application. The program becomes a festering mass of code that the developers find increasingly hard to maintain."

There’s more, but if I told you all of it I’d have to send you to the dermatologist.

If you have good tests, you can protect the reliability of any smelly pile of software, even if it makes you cry when you have to change the code. $$’s fly out the window but it all still works.

If you have good tests AND good design, the software will be reliable and changes will be an economical pleasure. You’ll look forward to adding new features so you can undertake big refactorings and make the code do even more.

But, enough with all this talk. Let’s do something.

Dependency Injection

Bob calls it Dependency Inversion and you could definitely argue that the two concepts are slightly different, but don’t quibble with me about this. I’m being practical here.

Example 1:

class Job
  def run
    @retriever = FileRetriever.new
    strm = @retriever.get_file(‘theirs’)
    
    @cleaner = FileCleaner.new 
    cleaned = @cleaner.clean(strm)
    
    local = ‘mine’
    File.open(local, ‘w’) {|f| f.write(cleaned) }
    local
  end
end

Class Job is responsible for retrieving a remote file, cleaning it up and then storing it locally. It uses two preexisting classes, FileRetriever and FileCleaner, which themselves have thorough tests.

The Job class is dirt simple. If you wrote it test first, you might have a spec like:

it “should retrieve ‘theirs’ and store it locally” do
    @job = Job.new
    local_fn = @job.run
    local_fn.should have_the_correct_contents
  end

What does this spec test? Job? Or Job AND FileRetriever AND FileCleaner? Obviously, all three. My spec is testing a whole set of objects; Job and all of it’s dependencies. It’s fragile in that it relies on objects other than the one under test and it runs too long because it exercises code that it should not care about.

Mocks/stubs to the rescue, right? I could stub FileRetriever.get_file and FileCleaner.clean and bypass both of those problems. However, even if I stub those methods, my code still has a bad smell. Stubbing improves the test but does not fix the flaw in the code.

Because of the style of coding in Job, it contains dependencies that effect my ability to refactor and reuse it in the future. Let’s move some code around.

Example 2:

class Job
  attr_reader :retriever, :cleaner, :remote, :local
  
  def initialize(retriever=FileRetriever.new, cleaner=FileCleaner.new, 
                  remote=‘theirs’, local=‘mine’)
    @retriever = retriever
    @cleaner   = cleaner
    @remote    = remote
    @local     = local
  end
  
  def run
    strm    = retriever.get_file(remote)
    cleaned = cleaner.clean(strm)
    
    File.open(local, ‘w’) {|f| f.write(cleaned) }
    local
  end
end

Now I’m injecting the dependencies into Job. Suddenly, Job feels a lot less specific and a lot more re-usable. In my spec I can create true mock objects and inject them; I don’t have to stub methods into existing classes.

That stylistic change helped a lot, but what if I want to provide some, but not all, of the arguments? It’s easy, just change the initialize method. It wouldn’t bother me if you also wanted to simplify run.

Example 3:

class Job
  attr_reader :retriever, :cleaner, :remote, :local
  def initialize(opts)
    @retriever = opts[:retriever] ||= FileRetriever.new
    @cleaner   = opts[:cleaner]   ||= FileCleaner.new
    @remote    = opts[:remote]    ||= ‘theirs’
    @local     = opts[:local]     ||= ‘mine’ 
  end
  
  def run
    File.open(local, ‘w’) {|f| 
      f.write(cleaner.clean(retriever.get_file(remote)))}
    local
  end
end

That feels really different from example 1. A simple change in coding style made Job more extensible, more reusable and much easier to test. You can write code in this style for no extra cost, so why not? It will save someone pain later.

Example 4 – Pain:

Here’s some code from Rails that generates xml for ActiveRecord objects. (Please, I’m not picking on them, this just happens to be a good example that I dealt with recently.)

module ActiveRecord #:nodoc:
  module Serialization
    def to_xml(options = {}, &block)
      serializer = XmlSerializer.new(self, options)
      block_given? ? serializer.to_s(&block) : serializer.to_s
    end
    #…
  end
end

Without recounting the whole story, I wanted to use to_xml with a different Serializer class. Imagine how easy it would be if XmlSerializer had been injected into to_xml. Instead, look at it and despair. I have to override the entire method just to name a different serializer class, with all the future maintenance burden that the change entails.

The to_xml code does exactly what it’s supposed to do and in that way cannot be faulted. The person who wrote it isn’t bad, they just never imagined that I would want to reuse it this way.

Let me repeat that.

They never imagined how I would reuse their code.

The moral of this story? The same thing is true for every bit of code you write. The future is uncertain and the only way to plan for it is to acknowledge that uncertainty. You do not know what will happen; hedge your bets, inject your dependencies.

TDD makes the world go ‘round. It lets us make unanticipated changes with confidence that our code will still work, but SOLID design principles keep the code ’clean, elegant, and compelling‘ after many generations of change.

Notes:

  1. I don’t mean to be overly familiar; it’s not like I know the man. But he’s an icon, how can I avoid calling him ‘Bob’?
Posted on March 21, 2009 .