Ruby (Rails) Best Practices (Part I)

February 21st, 2021

Hello, Friend

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.

Readability, In General

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

Chapter 1: Good code has clear intent. It does what it intends

1.1 Naming - General

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.

1.2 Naming - Objects

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

1.3 Naming - Functions and Methods

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.

# Poor
class WidgetSpinner
  def perform
  end
end

# Better
class WidgetSpinner
  def spin_widget
  end
end

class SpinWidgetJob
  def execute
  end
end

# Best
class Widget
  def spin
  end
end
Example 1.3.1

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).

1.4 Naming - Variables

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.

unicorn = Unicorn.first # A single variable, named for its type
unicorns = Unicorns.all # A simple enumerable comprised of a type

# A hash, named by its type
unicorns_by_horn_pattern = {
  Unicorn.first.horn_pattern => Unicorn.first,
  Unicorn.second.horn_pattern => Unicorn.second,
} 
Example 1.4.1

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.

sparkly_unicorns = Unicorns.sparkly.all
shiny_unicorns = Unicorns.shiny.all
Example 1.4.2

As seen in these examples, it’s helpful to also observe these hard rules:

1.5 Raise Errors

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:

  1. Aim to fail early with as much explanation as context allows. Typically, the place to start is with a custom error.
  2. Document the errors that are expected either in the immediate method or those in client methods that will be unhandled.
  3. Then, at the level at which it makes most sense, catch those errors and handle them.
  4. Do not suppress errors! Suppressing errors is extemely counterproductive because your code will not only fail but give you no clues for diagnosis.

Let us see what these tips look like in action.6

class Flamingo
  MIN_HEIGHT = 50 # cm

  # Define a simple custom error class 
  # The unique name alone is quite helpful for us to catch errors
  # and deal with them accordingly.
  class FlamingoNotFoundError < StandardError; end

  # Define a more nuanced custom error class
  # This can be used to capture relevant context that can be
  # extracted and used by error handling code
  class TooShortError < StandardError
    def initialize(name, height)
      # So callers can eg. tell the user which flamingos are too short
      @name = name 
      @height = height 
      @min_height = MIN_HEIGHT

      super("Proposed flamingo height of #{height} is too short.")
    end
  end

  #  
  # @param flamingo_name [String]
  # @param new_height [Numeric]
  #
  # @raise [Flamingo::TooShortError] If the given height is too short
  # @raise [Flamingo::NotFoundError] If the name does not match 
  #   any existing flamingos
  # 
  def self.update_height(flamingo_name, new_height)
    if flamingo.new_height < 0
      raise Flamingo::TooShortError.new(flamingo_name, new_height)
    end

    flamingo = Flamingo.find_by(name: flamingo_name)

    if flamingo.nil?
      raise Flamingo::NotFoundError.new
    end

    flamingo.update(height: new_height)
  end

end

...

# One of our client methods
# We are easily and accurately able to give the user pertinent
# information because of our (nicely documented) custom error classes
class FlamingoController < ApplicationController
  def update
    begin
      Flamingo.update_height(params[:name], params[:height])
      flash[:success] = "Flamingo height updated."
    rescue Flamingo::NotFoundError => e
      flash[:alert] = "No flamingo could be found with that name."
    rescue Flamingo::TooShortError => e
      # Use one of the fields we stored away in our custom error
      flash[:alert] = (
        "Flamingos must be at least #{e.min_height} cm tall."
      )
    end
    
    redirect_to :back
    return
  end
end
Example 1.5.1

1.6 Doc Blocks

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.

# @param bean_config [Hash] the configuration for a new jelly bean
#   @option :length [Decimal] in millimeters
#   @option :color [Array<Integer>] a 3-element array with ordered
#     RGB values
Example 1.6.1
## 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")`

Ex: (BAD) @param puppy_description [String] A description of the puppy.
Ex: (BETTER) @param puppy_description [String]

# BAD - describes how a function is used in client code
# This method is for used in the XYZ class for the ABC use case.

class Food
  ## BAD - Unnecessary explanation because the functions' purpose
  # and the meaning of the parameters is self-explanatory from name
  # A method that returns whether or not the food is edible 
  #
  # @return [Boolean] Whether or not the food is edible
  def edible?
    … (a one or two line implementation)
  end

  ## BETTER
  # @return [Boolean]
  def edible?
    .. (a one or two line implementation)
  end
end
Example 1.6.3

1.7 Comments

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:

# Iterate through medications to update them
self.medications.each do |med|
  med.update(...)
end
Example 1.7.1

I too can read code. However, if you aren’t sure of a comment’s value, err towards leaving it in.


  1. 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.

  2. Commonly known as the “Keep it simple, stupid” or KISS principle.

  3. “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.

  4. 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.

  5. 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.

  6. I turn to this this article when trying to remember what’s available via custom exceptions.