Fractal is about similar patterns recurring at progressively smaller scales. To me, good code is a fractal: you observe the same qualities repeated at different levels of abstraction.
This is not surprising. Good code is the one that is easy to understand, and the best mechanism we have to deal with complexity is building abstractions. These interchange complexity for an intelligible interface to us humans. But we still need to deal with that complexity they push down; to do that, we follow the same process all over: we build new abstractions that hide details and offer a higher-level mechanism to deal with those.
I am using abstraction to refer to everything: from a large subsystem to the last private method in some internal class. But how do you build those abstractions? Well, that’s the million-dollar question and the subject of countless books. In this post, I would like to focus on four qualities I consider essential when it comes to making code understandable:
- Domain-Driven: speak the domain of the problem.
- Encapsulation: expose crystal clear interfaces and hide details.
- Cohesiveness: do one single thing from the point of view of their caller.
- Symmetry: operate at the same level of abstraction.
Because this post is getting too, pardon me, abstract, I’ll clarify with some real-world code from Basecamp. In several places, the product offers an activity timeline. This timeline refreshes dynamically: it will update in real-time if someone does something while you are looking at it.
At the domain level, when you perform actions in Basecamp, such as completing todos, creating documents, or posting comments, the system creates events, and those events are relayed to several destinations, such as the activity timeline or webhooks. Let’s have a look at the code:
First, we have the
Event model, which includes a
Relaying concern (I am just showing the relevant parts):
class Event < ApplicationRecord
This concern adds an association
relays and a hook to relay events asynchronously when they are created:
after_create_commit :relay_later, if: :relaying?
class Event::RelayJob < ApplicationJob
Event#relay_now is the method we are interested in. Notice that it speaks the domain language; it does one thing from the point of view of the job that invokes it; and that everything involved in relaying an event is hidden at this point. Let’s dig into that method:
This method orchestrates the invocations to a set of lower-level methods. They are all about relaying, so cohesiveness remains; they have clear names for the relay destinations based on the domain; details are still hidden; and they are symmetric: you don’t have to jump across levels of abstraction to understand what this method does.
#relay_to_or_revoke_from_timeline looks like the one we are looking for:
Again, good domain-based names: it checks if a bucket is timelined and creates an object
Timeline::Relayer to relay events to a timeline; notice the symmetry: there is a counterpart class to revoke events; the method is cohesive, it focuses on relays and timelines, and implementation details remain hidden. Let’s look into that class:
@event = event
delegate :bucket, to: :event
bucket.record Relay.new(event: event), parent: timeline_recording, visible_to_clients: visible_to_clients?
TimelineChannel.broadcast_event(event, to: recipients)
This time the abstraction is a plain Ruby class, not a method, but we can observe the same traits. It exposes a public method
#relay that hides its implementation details. Looking inside, we see it does two operations: record the relay in the database and broadcast it via Action Cable (this code was written years before Hotwire). Notice the symmetry: even when both operations are a single-line invocation, they are extracted out as higher-level methods.
Finally, we reach the low-level details. The method
#record persists the relay in the database — relays are recordables for a recording, the seminal use case that originated Rails’ delegated types. And
#broadcast is the method where the event is broadcasted to recipients, the one we were interested in when we started.
In this example, we could easily understand the relaying logic from the moment an event is created until it is pushed through the action cable channel. We could do that because there is only one thing to pay attention to on each jump: one responsibility and one level of abstraction, with names reflecting the problem we are reasoning about. Of course, what constitutes good code is subjective and involves many more concepts, but the ability to make these journeys with ease on non-trivial systems is the number one quality in the code I like.
Sign up to get posts via email,
or grab the RSS feed.