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
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?There's more, but if I told you all of it I'd have to send you to the dermatologist.
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.
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 [1] 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 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.1 | class Job |
The Job class is dirt simple. If you wrote it test first, you might have a spec like:
1 | it "should retrieve 'theirs' and store it locally" do |
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:
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.1 | class Job |
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:
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.1 | class Job |
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.)
1 | module ActiveRecord #:nodoc: |
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'?
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'?
Hi Sandi,
ReplyDeleteGreat writeup! Nice simplification of dependency injection. So many people think of frameworks like Spring and others from the Java world every time someone talks about dependency injection.
Mark Menard
PS: We met at Obie Con in Boston in November.
I love "free" dependency injection. When you're dealing with old crap
ReplyDeletecode, you can break dependencies to make it more testable, but
maintain the same API by using default args as you've shown. This
makes your code more testable without breaking existing clients. Huge
win.
As for your to_xml example, I can go both ways on that. First there's
the idea that you shouldn't build flexibility in that you don't
need...the lack of a configurable serializer is not necessarily an
oversight on their part. You should refactor that flexibility in when
you need it. If you did that though, then wouldn't the first
iteration of every project be one big ol imperative script, with no
organization whatsoever, right?
There's a common theme running through the Rails code base, and that
theme is that it's not extensible using standard OOP techniques. They
have hook points for high-level behavior, but if you want to change
stuff under the hood then you're in for a lot of alias_method_pain.
I was about to say that the D in SOLID seems to be neglected in our
world...but I'd say that it's SOLID in general that is. Nice to see
you getting the word out! Also I'm attending GoRuCo, can't wait to
hang out again!!
Ah, Pat, the to_xml example is definitely the rub. It goes right to the heart of the issue: Is it good style to inject dependencies even when your current use case does not anticipate changes?
ReplyDeleteWhen I write code I've historically done just what they did, i.e., in the initialize methods of *my* objects I get new instances of the *other* objects I depend on. However, I keep getting burned, either because my code is hard to test or because I have an unexpected need to reuse the code with a different dependency.
I'm holding myself to two different standards. When I write framework code that I expect others to heavily reuse, I inject like mad even if I don't have a current need. In this case it feels like my obligation is to support unexpected collaborations among objects which have well defined APIs. This ability to easily make new collaborations is a design goal of the framework.
For non-framework code, I often still explicitly create the objects that I depend on. Mostly. Except when it's easier to write tests if I don't. And these dependencies lasts until I find a need for reuse and refactor the code..
As you can see, this is still of an experiment about dependency management. We'll see how it goes.
Excellent that you'll be at GoRuCo. I'm up for a face-to-face chat!
Glad to see the D of SOLID in Ruby is getting attention. What plugin do you use for your code snippets? They look nice :)
ReplyDelete@Sandi:
ReplyDelete> For non-framework code, I often still explicitly create the objects that I depend on.
This is ok. NTL I learned that you should at least refactor the creation of an object into an overridable factory method. This makes it easy to intercept or replace the creation if necessary. I seldom write 'new' in the middle of a statement block anymore.
@Michael The code is formatted with CodeRay, which rocks. It requires the CodeRay gem and something like this gist.
ReplyDelete@Erich I totally agree. Isolating the creation of a new object into a method of its own is a good compromise between injecting the class (complete separation) and creating it in the middle of a bunch of other code (complete entanglement). This is all about preserving the most future options at the lowest present cost.
what do rubyists think of DIP (depending on abstractions, not concretions?). Does this apply to Ruby at all?
ReplyDelete