Imagine driving from Denver, Colorado, to Death Valley National Park.
You get on I-70 and head west, climb the front range, cross the high peaks of the Rockies, descend Colorado’s western slope into Grand Junction and then make your way across the Utah dessert to Fishlake National Forest, which, if it’s springtime, is likely on fire. Here you collide with I-15 and turn left to head south.
In the deepest dark of night, eleven hours into your drive and two short of your destination, the long smear of the Milky Way Galaxy fades behind the neon lights of Las Vegas. Against your better judgement you stop and wander into a casino. They immediately recognize you as a programmer and force you to choose one of two sides in a permanent bet.
The bet is whether the code you wrote in the last month is ‘right’ or ‘wrong’. The casino defines ‘right’ as ‘Will not change for any reason within the next 6 months’. You’ll win if you bet on ‘right’ and your code doesn’t change, or if you bet on ‘wrong’ and it does.
The tricky part is this—you only get to choose once. You have to decide, right now, before you leave the casino, whether to bet all of your future income on each month’s code being ‘right’ or ‘wrong’. Once you chose, when you win the bet in a future month the casino will double your money, and when you lose, they’ll take your paycheck.
So, how do you bet, ‘right or ‘wrong’?
While you ponder this, let’s have a look at some code.
Example 1: Straightforward implementation
class House
def recite
(1..pieces.length).map {|i| line(i)}.join("\n")
end
def line(number)
"This is %s.\n" % pieces.last(number).join(' ')
end
private
def pieces
[
'the horse and the hound and the horn that belonged to',
'the farmer sowing his corn that kept',
'the rooster that crowed in the morn that woke',
'the priest all shaven and shorn that married',
'the man all tattered and torn that kissed',
'the maiden all forlorn that milked',
'the cow with the crumpled horn that tossed',
'the dog that worried',
'the cat that killed',
'the rat that ate',
'the malt that lay in',
'the house that Jack built',
]
end
end
This code produces the nursery rhyme ‘The House that Jack Built’. Let’s allow this simple example to stand proxy for a real world application.
What kinds of things might change?
- The recite method might change. For example, the reciters might decide that they sometimes want two newlines after each line.
- The hardcoded
‘This is’
string that starts each line might change. Perhaps some reciters feel more confident than others and wish to start lines with‘This definitely is’
. - The strings in pieces might change. Maybe the dog annoys the cat in addition to worrying it.
Any of these things might happen and you could add code, right now, to handle each possibility.
Perhaps you believe that some reciters will be more certain than others. Example 2 changes the code to support both the current and the anticipated requirement. It accepts a parameter (line 4) that’s used in an if statement (line 15) to determine how each line should start.
Example 2: Varying line_start
class House
attr_reader :definite
def initialize(definite=false)
@definite = definite
end
# ..
def line(number)
"#{line_start} %s.\n" % pieces.last(number).join(' ')
end
def line_start
definite ? "This definitely is" : "This is"
end
# ..
end
Now House.new.recite
displays the original rhyme and House.new(true).recite
the new one.
Example 3 shows an alternative that accomplishes the same thing another way. It avoids the if statement that was added in Example 2 by injecting the words that start each line, using a default (line 4) in order to continue to meet the current requirement.
Example 3: Injecting line_start
class House
attr_reader :line_start
def initialize(line_start="This is")
@line_start = line_start
end
# ...
def line(number)
"#{line_start} %s.\n" % pieces.last(number).join(' ')
end
# ...
end
Example 4 contains the complete listing using the code from Example 3.
Example 4: Injecting line_start (complete listing)
class House
attr_reader :line_start
def initialize(line_start="This is")
@line_start = line_start
end
def recite
(1..pieces.length).map {|i| line(i)}.join("\n")
end
def line(number)
"#{line_start} %s.\n" % pieces.last(number).join(' ')
end
private
def pieces
[
'the horse and the hound and the horn that belonged to',
'the farmer sowing his corn that kept',
'the rooster that crowed in the morn that woke',
'the priest all shaven and shorn that married',
'the man all tattered and torn that kissed',
'the maiden all forlorn that milked',
'the cow with the crumpled horn that tossed',
'the dog that worried',
'the cat that killed',
'the rat that ate',
'the malt that lay in',
'the house that Jack built',
]
end
end
Here House.new.recite
again displays the original rhyme and House.new("This definitely is").recite
the new one.
Notice that this code is a little more abstract than the original. This increase in abstraction makes it easier to change (as long as the change is the one you anticipated!) but slightly harder to understand.
Code is read many more times than it is written. Abstractions add changeability but increase cognitive load. The ones that are actually needed save money, those that aren’t increase costs every time anyone looks at the code.
Anticipatory complexity rarely pays off. Unless your Magic 8-Ball is far better than mine you should avoid the guessing business. Guessing right half of the time means guessing wrong the other half, and the code for wrong guesses confuses everyone who follows. The barrier to introducing a speculative abstraction is very high.
This is where the Open/Closed Principle (OCP) comes in handy. OCP is one of the core object-oriented design principles. It provides both the ‘O’ in ‘SOLID’ and guidance about when to create an abstraction.
Open/Closed is short for the phrase ‘Open for extension but closed for modification’, and this phrase, in turn, means that you should arrange things so that you can add new behavior to an application without changing its existing code.
If you find this idea incomprehensible, you’re not alone; this sounds quite impossible. However, suspend disbelief for a moment and start by imagining a world in which your applications are open/closed, where you can add new behavior without changing the code you have.
In this open/closed world two very powerful things are true:
It’s difficult to accidentally break existing code, and
the tests you have always run green.
This is programming nirvana. Open/Closed is clearly good; the problem isn’t that it’s wrong, it’s that it’s not obvious how to achieve it. We want our code to be open/closed to the next requirement but we cannot know what that requirement will be.
Are we doomed to guess? No.
Open/Closed requires that you write code that is open to the next change but it says nothing about when to do so. It doesn’t require that you guess the future, instead it tells you to write the simplest conceivable code today and then to remove your hands from the keyboard and wait. Future change requests will tell you exactly how you should have arranged the code you have.
When new requests arrive, you’ll rearrange existing code so that you can extend it with new behavior. It’s a two step process, first you refactor existing code to a more felicitous arrangement and then you write new code to implement the change. Kent Beck says it best (but I’ll paraphrase anyway): “Make the change easy … then make the easy change.”
In the absence of an explicit requirement to the contrary, Example 1 is a pleasingly simple way to arrange this code. Once you’re asked to sometimes start sentences with “This is” and other times with “This definitely is”, you’re forced to make a change.
Example 2 is not open/closed to this change. Line 15 interleaves the new code with the old, adding “This definitely is” into the existing mix, which stands a chance of breaking existing code and tests.
Example 3 / 4, in contrast, is open/closed. All of the code is needed by the existing requirement (no new behavior has been added) and rearranging this code to be open/closed to the new requirement was accomplished without changing the existing test. It can now be extended to meet the new requirement by simply passing in a new string. It’s open for extension and closed to modification.
The morals of this story are:
First, you don’t need to guess the future—sit back and wait; it will eventually arrive. Change requests are inevitably for something you did not anticipate.
Guesses serve only to muddy the cognition waters during the course of their insufficient lives.
Next, when a new request does arrive, make the change in two steps. Refactor existing code to be open to the new requirement and then “make the easy change”.
Finally, write simple code. It will likely be no more ‘right’ than your guesses but certainly will be easier to change when you figure out what you should have done.
This brings us full circle and solves your quandary in Vegas. Requirements are fleeting. Even code that is absolutely necessary today is likely to change in the future, and the odds of your guesses surviving the test of time are even longer.
The smart money bets on wrong.
News: 99 Bottles of OOP in JS, PHP, and Ruby!
The 2nd Edition of 99 Bottles of OOP has been released!
The 2nd Edition contains 3 new chapters and is about 50% longer than the 1st. Also, because 99 Bottles of OOP is about object-oriented design in general rather than any specific language, this time around we created separate books that are technically identical, but use different programming languages for the examples.
99 Bottles of OOP is currently available in Ruby, JavaScript, and PHP versions, and beer and milk beverages. It's delivered in epub, kepub, mobi and pdf formats. This results in six different books and (3x2x4) 24 possible downloads; all unique, yet still the same. One purchase gives you rights to download any or all.