February 21st, 2021
This guide opines on how to write readable and maintainable ruby code. Ruby offers a few good ways to perform any task and several notably bad ways. For common situations, this guide puts forth a default option. If you and readers of your code follow those defaults, you will disagree fewer times on matters of style and receive swift reviews. There are times when these defaults will not be appropriate. To that:
It is an old observation that the best writers sometimes disregard the rules of rhetoric. When they do so, however, the reader will usually find in the sentence some compensating merit, attained at the cost of the violation. Unless [you are] certain of doing as well, [you] will probably do best to follow the rules.1
This applies as well to writing code as it does to writing English. Break the rules only when you have adequate rationale.
As a rule, your code will be read more often than it is amended. Your readers may be junior, they may be from other teams, and, in almost all cases, they will be unfamiliar with your intent. As a software engineer - as opposed to a hobbyist programmer - it is a primary function of your job to write code that can be maintained. Readable code is a necessary (if insufficient) prerequisite to maintainable code.
While an imprecise target, readable code possesses these attributes, which are used to organize the remainder of this guide.
Breaking the KISS principle is the most common way very smart programmers write unmaintainable code.3
Frequently, the KISS principle will be violated by
All of these things are useful tools and should be used when simple code cannot reasonably do what it needs to. They should be used sparingly. Typically there comes a recognizable tipping point for when they become necessary.
Many of the tactics in this guide encourage you to use restraint when using all the clever things your computer science degree or Design Patterns taught you. I enjoy design patterns. Most of the time, they obfuscate the code’s purpose.4
Good names are expressive and concise. Names ought to be as specific as necessary for the context but not more. Good names avoid jargon when possible, especially programming jargon. Business domain jargon is acceptable, when needed.
Objects (classes, generally) are. Consequently, name objects with nouns. There’s a huge corollary here that you may need not make an object if your intent is to execute an action rather than to represent a thing.5
Functions do. Consequently, name functions with verbs. Expressivity is key to function names. “Perform”, “Run”, “Execute”, and “Do” are useless names except in the context of a job or service object. Even then, consider if instance or static methods might do better.
When choosing what verb to use, try to be precise and consistent. Consider the common example of a method on an ActiveRecord model that retrieves some information from a model or service object built around a database table. ‘Get’ may be sufficient, but there are a lot of operations that do more than just ‘get’. In these situations with common operations, try to develop a precise nomenclature and apply it uniformly. In this case, I suggest:
When naming a method, assume the method will succeed. For example, even if manipulating inventory data may throw a database locking error set_onhand_quantity
is a better name than attempt_to_set_onhand_quantity
. If a method can throw an error in normal use, those errors should be anticipated, documented, and handled by the client code according to the use case (see Errors & Return Types, Doc Blocks).
Variables are especially context sensitive. When you are writing a simple method, you can often get away with using the type as a proxy for the identity of a variable. The following examples are clear in that they express both identity and type.
Type becomes an insufficient proxy for identity once more than one variable of a type enters the same context. In this case, we need to be more specific. Our objective now is to obviously distinguish between variables of the same type.
As seen in these examples, it’s helpful to also observe these hard rules:
<value>s_by_<key>
When using a method, we should be allowed to assume it will succeed. To guarentee this assumption, a method must raise an error on failure. A method that both fails to complete it’s objective and does so without complaint is nearly impossible to design around.
Being clever, for example returning nil rather than the expected type, results in unpredictable behavior. Often the nil will pass back through the execution stack some way before the code attempts to use it and raises an error. At that point, the error diagnosis becomes a forensic expedition. A clear cut error at the moment of failure saves us from this.
Instead:
Let us see what these tips look like in action.6
Ruby is weakly typed for flexibility, not to advantage laziness. You should know the appropriate types for inputs, outputs, and expected exceptions from your function and communicate them to future readers of the code. We capture this information through YARD formatted doc-blocks.
Here are the YARD-doc docs, but I find it’s easier to start with this cheatsheet of examples. In addition to the basic formatting of YARD, I’ve found it’s best to apply these rules in Pillpack code, even where they deviate from the original YARD standard.
@<tag> <name> [<type>] <desc>
order. This clearly delineates the name from the description. Ex: @param best_ferrets [Array<Ferret>]
a list denoting the specific ferrets to which this method will dispense treatsFor a parameter that returns the result of an ActiveRecord where
method on the Narwhal model [ActiveRecord::Relation
find_by
on the Tomcat model would be written as @return [Tomcat|Nil]
Describe all the expected keys within hash parameters where it is realistically possible to do so.
Include examples in doc blocks if your function call needs precise configurations, requires hash parameters, uses other unusual types of parameters, or introduces some type of metaprogramming to the caller.
Document argument names and defaults as follows. This informs the caller both of the default values (if any) and the syntax with which they need to invoke the function.
## Required positional parameters
# ...
# @param bar [String] …
# ...
def foo(bar)
## Optional positional parameters
# ...
# @param bar="baz" [String] …
# ...
def foo(bar="baz")
## Required keyword parameters
# ...
# @param bar: [String] …
# ...
def foo(bar:)
## Optional keyword parameters
# ...
# @param bar: "baz" [String] …
# ...
def foo(bar: "baz")`
Omit descriptions of how a function is currently used in client code or, generally, how you intend callers to use your function. Doing so is a form of soft implicit coupling. When that client code is changed, the author must amend your now-inaccurate doc block. Inevitably they will not.
Omit parameter descriptions where the parameter name is self-explanatory.
Ex: (BAD) @param puppy_description [String] A description of the puppy.
Ex: (BETTER) @param puppy_description [String]
Ruby is quite expressive. Your code itself should clearly explain what it does. If it does not, refactor your code for clarity. If you must leave a comment to explain your shorthand, write your code longform instead.
Use comments, instead, to explain why it does something in four primary situations: - Explanation of requirements - Workarounds for subtle issues - Using common tools in uncommon ways (eg. puts # new line
) - Deviations from the ordinary way of doing things (eg. for performance optimization)
Do not comment unnecessarily. Extra comments lull the reader into ignoring useful ones. For example, I often see:
I too can read code. However, if you aren’t sure of a comment’s value, err towards leaving it in.
Strunk and White’s Elements of Style - As a programmer, you are a writer, albeit with a much reduced vocabulary. This book is the best book I’ve ever read on the topic of writing. It’s only 50 pages long and in the public domain. I encourage you to read it.↩
Commonly known as the “Keep it simple, stupid” or KISS principle.↩
“The competent programmer is fully aware of the limited size of their own skull. They therefore approach their task with full humility and avoid clever tricks like the plague.” - Edsger Dijkstra.
We used to have two very smart programmers at a company where I worked. They wrote very clever, elegant code. One even wrote his own deployment framework. It took latter day programmers days of careful study of their elegant, poorly documented, wholly unmaintainable code to make even simple changes, even as they marvelled and cursed at its beauty.
The company even wrote its own locking system on top of Redis! It was widely used. It took three years to realize that it increased the incidence of race conditions. Now we use Postgres locking because that’s what it’s for.↩
On the topic though, I recommend Robert Nystrom’s Game Programming Patterns, available for free in its beautifully formatted web edition. It’s the most approachable and enjoyable presentation of design patterns I’ve read to date and - critically - includes advice on alternatives and when not to use patterns.↩
Though more of an opinion than a recommendation, I suggest reading the Steve Yegge blog post Execution in the Kingdom of Nouns. It argues that a program’s purpose is to do something. In the English language, verbs do things. A program written with excessive focus on objects (looking at you, Java), or nouns, poorly models a program’s purpose to do a thing. In brief, don’t write a ThingDoer object when a method suffices.↩
I turn to this this article when trying to remember what’s available via custom exceptions.↩