It's Harder to Read Code Than Write It
- 5 minutes read - 865 wordsIn Things You Should Never Do, Part I, Joel Spolsky narrates Netscape’s ruinous decision to rewrite their browser from scratch. This introduced the following concept to me: “It’s harder to read code than to write it.” I believe this is true. Today I’d like to explain why.
A Practical Example
As an example, consider the “liking” feature of Today I Learned. This is an icon under each Today I Learned post that you can click to indicate: “I like this post.”
When we explore TIL’s codebase, we see this method called when a visitor clicks the icon. I’m going to examine this method line-by-line and raise questions that I might raise while coding, as if I’d never seen it.
# app/models/posts.rb
def increment_likes
self.max_likes += 1 if self.max_likes == self.likes
self.likes += 1
notify_slack_on_likes_threshold if likes_threshold?
save
end
First line:
# app/models/posts.rb
def increment_likes
self.max_likes += 1 if self.max_likes == self.likes
self.likes += 1
notify_slack_on_likes_threshold if likes_threshold?
save
end
This method name suggests that likes
is an attribute on the model. If true, why do we
need a custom method? It’s not obvious.
Next:
# app/models/posts.rb
def increment_likes
self.max_likes += 1 if self.max_likes == self.likes
self.likes += 1
notify_slack_on_likes_threshold if likes_threshold?
save
end
😵💫 Okay, what is max_likes
? ‘Max’ as a prefix is pretty vague. Why
are we comparing that to likes
and only incrementing it when they’re equal?
This comparison would benefit from abstraction to a method like
new_max_likes?
, or abstracting the whole line to maybe_increment_max_likes
.
Then:
# app/models/posts.rb
def increment_likes
self.max_likes += 1 if self.max_likes == self.likes
self.likes += 1
notify_slack_on_likes_threshold if likes_threshold?
save
end
Incrementing the likes; I think I understand this.
Next:
# app/models/posts.rb
def increment_likes
self.max_likes += 1 if self.max_likes == self.likes
self.likes += 1
notify_slack_on_likes_threshold if likes_threshold?
save
end
So we’re notifying Slack about something called likes_threshold
, if
likes_threshold?
is true. This abstraction feels incomplete. Why can’t
notify_slack_on_likes_threshold
know if we’re in the right state? Once
we refactor it, maybe we could give it name like maybe_notify_slack
.
Last line:
# app/models/posts.rb
def increment_likes
self.max_likes += 1 if self.max_likes == self.likes
self.likes += 1
notify_slack_on_likes_threshold if likes_threshold?
save
end
Why explicitly save? Could this be combined with the likes incrementing via
.update
?
That finishes the method. It’s short enough to appease Sandi Metz. But what it’s doing is not obvious.
Missing Context
There’s a big piece of information missing from this analysis: this method exists to let us send idempotent notifications to Slack.
When we built TIL, we wanted to know which posts were popular, so we added liking. We chose to send messages to Slack when these likes crossed important thresholds, such as 100 likes.
max_likes
came from this. Before it, if somebody clicked the like icon with
nine likes from other users, preparing to increment the count to ten, then
clicked it again (an un-click), and then clicked it again, our code would send
two Slack notifications saying the post had achieved ten likes. We needed
idempotency, hence max_likes
, which tracks the highest amount of likes that
has ever been for a post.
That feature was important, because it proved that our technical writing was being enjoyed by an audience. As we saw likes increment, we saw what was popular. It’s a little thing that helped made TIL a hit.
Just reading this code does not expose this context.
Impact of Missing Context
Practically, what happens when this context is absent?
If you’re rewriting an application from scratch, these questions hardly ever get asked. Given you even have a copy of the code, you can’t take the time to explore every method in detail. You’re speed-running. Whoever wrote the feature is long gone.
In the unlikely event you do read this method, you’re going to say: “What’s happening here? I don’t get it. This isn’t MVP” and move on. This kind of omission is essential in a rewrite. You’re only building the stuff that’s important, with bias, ignorance, and inattention coloring that definition.
That’s why it’s harder to read code than to write it. Writing is easy, because you know everything about your problem. Reading code forces you to consider somebody else’s problems, that possibly appeared at strange times and scales, possibly over a span of years. That’s hard.
Challenging the Paradigm
As gnarly as legacy code is, it’s a Chesterton’s Fence and people have to come to rely on it. That is worth our attention. When we read legacy code thoughtfully, we make better decisions.
At AWS they almost never retire an API. Once it’s in production, it’s supported forever. Many companies can’t act this way, but the spirit prevents thoughtless mistakes like breaking the likes notification.
I think code reading is the highest programming skill you can have. Scanning a line, putting it in words, precisely jumping to a function definition in multiple codebases, returning without getting lost, and adding context to the bigger story you’re telling yourself or those participating– doing all of these well is rare. I want to encourage us all to work on being better code readers.
“It’s harder to read code than to write it.” What does this statement mean to you?
Thanks to Josh Branchaud for this blog post idea.