Jake Worth

Jake Worth

It's Harder to Read Code Than Write It

Published: April 25, 2022 • Updated: August 23, 2023 4 min read

  • rewrites

In 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?
  saveend

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.

What are your thoughts on reading code? Let me know!


Join 100+ engineers who subscribe for advice, commentary, and technical deep-dives into the world of software.