The latest Basecamp for iPhone release involved an immense amount of refactoring with how HTTP requests and HTML pages were rendered. In a previous release, my coworker Sam wrote several methods that didn’t serve to reduce duplication, but instead their purpose was to increase clarity. Since the focus is on comprehension rather than just reusability, it’s easier to understand what is going on and jump in to solve the next problem.
This pattern has been eye-opening, and I’ve been using it to hide implementation details and remove comments explaining what chunks of code are doing. Instead, the name of the method explains what is going on. The gist here to communicate Intention Not Algorithm:
The most important thing about any (without loss of generality) method is what it accomplishes or why one would call it, not how it does whatever it does.
Here’s one example from Basecamp for iPhone of this. Since we make heavy use of web views, we need to keep track of when the page loaded with content. How we accomplish this actually doesn’t matter, but the why matters greatly.
In this method that is mixed into every UIViewController
that makes requests via AFNetworking, the intent of remembering when the page loaded feels tangled up with the code surrounding it:
module Browser
def enqueue(request)
operation = client.HTTPRequestOperationWithRequest(request, success:
lambda { |operation, response|
normalRenderer.new(self, operation).render(response)
@pageLoaded = true
}, failure: lambda { |operation, error|
fallbackRenderer.new(self, operation).render
})
client.enqueueHTTPRequestOperation(operation)
end
end
All we need is an instance variable to keep track of when a page has been loaded, but if you come back to where that variable is set on its own, it’s confusing. The how doesn’t matter here, but the why does. Let’s try pulling that out into its own intention revealing method:
module Browser
def enqueue(request)
operation = client.HTTPRequestOperationWithRequest(request, success:
lambda { |operation, response|
normalRenderer.new(self, operation).render(response)
markPageAsLoaded
}, failure: lambda { |operation, error|
fallbackRenderer.new(self, operation).render
})
client.enqueueHTTPRequestOperation(operation)
end
def markPageAsLoaded
@pageLoaded = true
end
end
Now, the intention is clear and the implementation is just details. A simple attr_writer :pageLoaded
could have accomplished the same job, but it still doesn’t give a clear message as to why it was called.
Our recent release focused heavily on improving offline detection. This module’s viewWillAppear
method listens for two custom NSNotificationCenter
events that fire when the device goes on and offline:
module Offline
def viewWillAppear(animated)
super
on "WentOffline" do |notification|
showOffline
end
on "BackOnline" do |notification|
restoreFromOffline
end
end
def showOffline
if isPageEmpty
OfflineRenderer.new(self).render
markPageAsLoaded
end
end
def restoreFromOffline
request if client.isInactive
end
end
I could have shoved the code for what happens when the device switches states into their respective blocks, but now the intent is clear. Did the device go offline? Show the offline page (unless if something was on the page previously). We’re back online? Restore the controller from the offline state by requesting the page again.
Give this a try the next time you come across some code or a comment that covers What But Not The Why, instead of leaving it as a knot to unravel months later.
Ben Orenstein
on 11 Jun 13I wrote up some very similar thoughts on the thoughtbot blog: Code that says Why it Does.
Matt De Leon
on 11 Jun 13Nick, you just inspired me to change this complicated one line method:
last_notification_enqueued_at.nil? || (noteworthy_changes_made? && no_notification_pending?)
to
no_notification_has_ever_been_sent? || (noteworthy_changes_made? && no_notification_pending?)
Who cares about the extra characters? :)
p.s. your advice applies equally well to css, html, css querying in jquery, etc.
Jason
on 11 Jun 13I couldn’t agree with you more. Maintainability/legibility is more important that squeezing the last drop out of performance in the majority of cases. I run into this in databases regularly (I have to support a few .NET applications) where things are what I call “over-normalized”. There’s even a look up table for the name/description of values from other look up tables in one of the apps!
I always try to name things as clearly as possible and define things as logically as possible, rather than worrying about whether I can remove one extra line or a few extra characters.
Juha
on 12 Jun 13Thats a nice thought, thanks! Lets’ capsulate how’s into why’s.
Josh Smith
on 14 Jun 13If you’re hoping to see a whole book chock full of this stuff, you should really read Jim Gay’s Clean Ruby.
This discussion is closed.