Confident Ruby
Introduction
Ruby is designed to make programmers happy.
Yukhiro "Matz" Matsumoto
This is a book about Ruby, and about joy.
If you are anything like me, the day you first discovered the expressive power of Ruby was a very happy day. For me, I think it was probably a code example like this which made it "click":
. times do puts "Hello, Ruby world!" end
To this day, that's still the most succinct, straightforward way of saying "do this three times" I've seen in any programming language. Not to mention that after using several supposedly object-oriented languages, this was the first one I'd seen where everything, including the number "3", really was an object. Bliss!
Ruby meets the real world
Programming in Ruby was something very close to a realization of the old dream of programming in pseudocode. Programming in short, clear, intent-revealing stanzas. No tedious boilerplate; no cluttered thickets of syntax. The logic I saw in my head, transferred into program logic with a minimum of interference.
But my programs grew, and as they grew, they started to change. The real world poked its ugly head in, in the form of failure modes and edge cases. Little by little, my code began to lose its beauty. Sections became overgrown with complicated nested if/then/else
logic and &&
conditionals. Objects stopped feeling like entities accepting messages, and started to feel more like big bags of attributes. begin/rescue/end
blocks started sprouting up willy-nilly, complicating once obvious logic with necessary failure-handling. My tests, too, became more and more convoluted.
I wasn't as happy as I once had been.
Confident code
If you've written applications of substantial size in Ruby, you've probably experienced this progression from idealistic beginnings to somewhat less satisfying daily reality. You've noticed a steady decline in how much fun a project is the larger and older it becomes. You may have even come to accept it as the inevitable trajectory of any software project.
In the following pages I introduce an approach to writing Ruby code which, practiced dilligently, can help reverse this downward spiral. It is not a brand new set of practices. Rather, it is a collection of time-tested techniques and patterns, tied together by a common theme: self confidence.
This book's focus is on where the rubber meets the road in object-oriented programming: the individual method. I'll seek to equip you with tools to write methods that tell compelling stories, without getting lost down special-case rabbit holes or hung up on tedious type-checking. Our quest to write better methods will sometimes lead us to make improvements to our overall object design. But we'll continually return to the principal task at hand: writing clear, uncluttered methods.
So what, exactly do I mean when I say that our goal is to write methods which tell a story? Well, let me start with an example of a story that isn't told very well.
A good story, poorly told
Have you ever read one of those "choose your own adventure" books? Every page would end with a question like this:
If you fight the angry troll with your bare hands, turn to page 137.
If you try to reason with the troll, turn to page 29.
If you don your invisibility cloak, turn to page 6.
You'd pick one option, turn to the indicated page, and the story would continue.
Did you ever try to read one of those books from front to back? It's a surreal experience. The story jumps forward and back in time. Characters appear out of nowhere. One page you're crushed by the fist of an angry troll, and on the next you're just entering the troll's realm for the first time.
What if each individual page was this kind of mish-mash? What if every page read like this:
You exit the passageway into a large cavern. Unless you came frompage 59, in which case you fall down the sinkhole into a largecavern. A huge troll, or possibly a badger (if you already visitedQueen Pelican), blocks your path. Unless you threw a button down thewishing well on page 8, in which case there nothing blocking yourway. The [troll or badger or nothing at all] does not look happy tosee you.
If you came here from chapter 7 (the Pool of Time), go back tothe top of the page and read it again, only imagine you are watchingthe events happen to someone else.
If you already received the invisibility cloak from the agedlighthouse-keeper, and you want to use it now, go topage 67. Otherwise, forget you read anything about an invisibilitycloak.
If you are facing a troll (see above), and you choose to run away,turn to page 84.
If you are facing a badger (see above), and you choose to run away,turn to page 93
Not the most compelling narrative, is it? The story asks you to carry so much mental baggage for it that just getting through a page is exhausting.
Code as narrative
What does this have to do with software? Well, code can tell a story as well. It might not be a tale of high adventure and intrigue. But it's a story nonetheless; one about a problem that needed to be solved, and the path the developer(s) chose to accomplish that task.
A single method is like a page in that story. And unfortunately, a lot of methods are just as convoluted, equivocal, and confusing as that made-up page above.
In this book, we'll take a look at many examples of the kind of code that unnecessarily obscures the storyline of a method. We'll also explore a number of techniques for minimizing distractions and writing methods that straightforwardly convey their intent.
The four parts of a method
I believe that if we take a look at any given line of code in a method, we can nearly always categorize it as serving one of the following roles:
- Collecting input
- Performing work
- Delivering output
- Handling failures
(There are two other categories that sometimes appear: "diagnostics", and "cleanup". But these are less common.)
Let's test this assertion. Here's a method taken from the MetricFu project.
def location ( item , value ) sub_table = get_sub_table ( item , value ) if ( sub_table . length == ) raise MetricFu : :AnalysisError , "The #{ item . to_s } ' #{ value . to_s } ' " \ "does not have any rows in the analysis table" else first_row = sub_table [ ] case item when :class MetricFu : :Location . get ( first_row . file_path , first_row . class_name , nil ) when :method MetricFu : :Location . get ( first_row . file_path , first_row . class_name , first_row . method_name ) when :file MetricFu : :Location . get ( first_row . file_path , nil , nil ) else raise ArgumentError , "Item must be :class, :method, or :file" end end end
#location method from MetricFu
Don't worry too much right now about what this method is supposed to do. Instead, let's see if we can break the method down according to our four categories.
First, it gathers some input:
sub_table = get_sub_table ( item , value )
Immediately, there is a digression to deal with an error case, when sub_table
has no data.
if ( sub_table . length == ) raise MetricFu : :AnalysisError , "The #{ item . to_s } ' #{ value . to_s } ' " \ "does not have any rows in the analysis table"
Then it returns briefly to input gathering:
else first_row = sub_table [ ]
Before launching into the "meat" of the method.
when :class MetricFu : :Location . get ( first_row . file_path , first_row . class_name , nil ) when :method MetricFu : :Location . get ( first_row . file_path , first_row . class_name , first_row . method_name ) when :file MetricFu : :Location . get ( first_row . file_path , nil , nil )