Amir Sharif
Engineer.
Weekend hacker.
Self improvement enthusiast.

Clean Code: A Primer for Ruby

I wanted a primer/cheatsheet for myself from the book Clean Code, and generated it to be tailored to Ruby.

Table of Contents

  • Meaningful Names
  • Functions
  • Comments
  • Formatting
  • Objects and Data Structures
  • Error Handling
  • Boundaries
  • Unit Tests
  • Classes
  • Systems
  • Emergent Design
  • Code Smells

Meaningful Names

  • Descriptive & Intention-Revealing: Use clear, expressive names that reveal intent. Avoid one-letter variables or cryptic abbreviations — readers should understand what a name stands for at a glance.
  • Consistent Conventions: Follow Ruby naming conventions (use snake_case for methods/variables, CamelCase for classes/modules, SCREAMING_SNAKE_CASE for constants). Consistency makes code easier to read.
  • Avoid Misleading Names: Don’t use names that might confuse — for example, don’t name a variable list if it’s not a list, or use similar names for different things. Make each name unambiguous.
  • No Prefixes or Hungarian Notation: Don’t encode types or scopes in names. For instance, in Ruby we don’t prefix booleans with is_ (instead, use meaningful predicate names ending in ? like empty?), and we avoid type tags like user_str or num_value.
  • Add Context When Needed: Provide sufficient context in names. If there are two dates, start_date and end_date are clearer than date1 and date2. Conversely, don’t add redundant context — inside a User class, a method can be just #name instead of #user_name.
# Before
a = 10
b = "John"
x = 15

# After
age = 10
first_name = "John"
number_of_students = 15

Functions

  • Small & Focused: Write small methods that do one thing and do it well. If a method is doing multiple steps (e.g. validating, processing, and outputting), split it into separate methods for each step.
  • Minimal Parameters: Limit the number of arguments a function takes (0-3 is ideal). If you have many related parameters, group them into a single object or hash for clarity. This makes APIs easier to use and reduces mistakes in argument ordering.
  • No Flag Arguments: Avoid boolean parameters that alter a function’s behavior — that’s often a sign the function is doing more than one thing. Instead, split into two methods (for example, create_user and create_admin_user instead of a create_user(is_admin) flag).
  • Avoid Side Effects: Functions should not unexpectedly modify global state or their input arguments. A method that changes an object it didn’t create or a global variable can surprise callers. Return new values or results instead of changing external state, unless side effects are intended and clearly indicated.
  • Use Guard Clauses: Embrace Ruby’s expressiveness — return early to handle edge cases or invalid input. This keeps the normal path less indented and more readable. For example, use return unless valid?(x) at the top of a method to avoid deep nested if blocks.
# Before: one method doing too much
def process_data_and_save(input)
  # 1. Validate input
  # 2. Transform data
  # 3. Save to database
end

# After: split into distinct responsibilities
def process_data(input)
  validate(input)
  transformed = transform(input)
  save_to_db(transformed)
end

def validate(input)
  # validation logic...
end

def transform(input)
  # data transformation...
end

def save_to_db(data)
  # saving logic...
end

Comments

  • Code Should Explain Itself: Strive to write self-explanatory code so that comments are largely unnecessary. If the code is clear (with good names and structure), you don’t need a comment to explain what it’s doing.
  • Comment Why, Not What: When you do write comments, focus on the intent or reasoning behind the code, not a line-by-line narration of what the code does. For example, document the purpose of a complex calculation or why a certain workaround is in place.
  • Avoid Redundant Comments: Comments that restate the obvious or outdated comments that no longer match the code are noise. It’s better to delete them — they can mislead.
  • Use Comments Sparingly: Prefer self-documenting constructs (clear names, well-defined functions) over explanatory comments. If you feel compelled to write a long comment to explain a section, consider refactoring that code instead.
  • No Commented-Out Code: Remove unused or dead code rather than commenting it out. Version control preserves history if you need to see old code. Leaving it in comments only clutters the codebase.
# Before
# Increment i by 1
i += 1

# After
i += 1  # advance to the next index in the loop

Formatting

  • Consistent Style: Follow a consistent coding style for whitespace, indentation, and braces. In Ruby, use soft tabs with two-space indentation and typical formatting conventions (space after commas, around operators, etc.). Consistency makes it easier for anyone to read and navigate the code.
  • Logical Spacing: Organize code with line breaks to separate logical sections. For example, use blank lines between method definitions and between chunks of code in a method to group related operations. This vertical openness” helps readers see the structure at a glance.
  • Reasonable Line Length: Keep lines of code reasonably short (around 80 characters is a common guideline) to avoid horizontal scrolling and make code easier to read in editors and side-by-side diffs. Long expressions can often be split or extracted into variables for clarity.
  • One Statement per Line: Each line should ideally do only one thing. Avoid writing multiple statements in one line (e.g., x = 1; y = 2; print x+y) as it hampers readability. Breaking them into separate lines makes it clear and git diffs more precise.
  • Automate Formatting: Leverage tools like RuboCop to detect and enforce style issues automatically. This helps maintain formatting standards across the team (spaces, alignment, etc.) and frees you to focus on logic rather than nitpicky formatting details.
# Before (poor formatting and style)
def sum(a,b)
result=a+b
return result
end

# After (idiomatic Ruby formatting)
def sum(a, b)
  result = a + b
end

Objects and Data Structures

  • Encapsulate Data with Objects: Combine data and behavior in classes when appropriate. If a set of data belongs together and has associated operations, use a class to encapsulate it. This way, logic and data remain coupled, and you can add behavior without exposing internals.
  • Use Simple Structs for Plain Data: If you just need to group data with no behavior (like configuration or lightweight records), simple structures like hashes, arrays, or Struct/OpenStruct can be used. But as soon as you start adding related behavior, that data deserves a class of its own.
  • Tell, Don’t Ask: Instead of pulling data out of an object to manipulate it, tell the object to do the work itself. For example, rather than if !user.active then user.activate end, just call user.activate and let the User class handle the details. This keeps logic in the appropriate place.
  • Law of Demeter (Least Knowledge): A given object should know little about the internal details of others. Avoid train-wreck” chains like order.customer.address.zip_code — they make you reach through multiple objects. Instead, expose a method (e.g., order.shipping_zip) or have the needed logic inside the object to prevent external chaining.
  • Avoid Hybrid Structures: Don’t design objects that are half object, half data structure (i.e., classes that expose all their data via getters/setters but also have behavior). Either keep it as a simple data container or, better, hide the data and provide methods. Exposing internal data then manipulating it externally defeats encapsulation.
# Before: using a hash for structured data
user_data = { name: "Alice", age: 25 }

# After: using a class to encapsulate data
class User
  attr_accessor :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end
end

user = User.new("Alice", 25)

Error Handling

  • Use Exceptions, Not Special Values: Handle error conditions by raising exceptions rather than returning error codes or nil silently. Exceptions make sure the error can’t be ignored unintentionally and separate normal flow from error flow.
  • Define Meaningful Exceptions: Use Ruby’s exception hierarchy or create custom exception classes for your domain (e.g., AuthenticationError) to represent different error scenarios clearly. This provides more context when rescuing and handling errors.
  • Catch at the Right Level: Don’t blanket-rescue exceptions at low levels unless you can handle them meaningfully. Let exceptions bubble up to a level that knows what to do (for example, the controller level can rescue and show an error to the user, whereas a deep library function might not know how to handle it). Avoid swallowing exceptions completely (e.g., an empty rescue block).
  • Clean Up Resources: Use ensure blocks (or Ruby’s blocks that manage resources, like File.open with a block) to guarantee cleanup (closing files, releasing locks) even if an exception occurs. This keeps the system stable in the face of errors.
  • Avoid Returning nil for Errors: In Ruby, nil might be a valid value, so using it to signal error” can be ambiguous. Prefer exceptions for truly exceptional cases. If a missing result is expected (e.g., search not found), consider using nil with documentation, or better, use Result objects or Either to make success/failure explicit.
# Before: handling an error by printing and returning
def divide(a, b)
  if b == 0
    puts "Cannot divide by zero"
    return nil   # or some error code
  end
  a / b
end

# After: handling an error by raising an exception
class DivisionByZeroError < StandardError; end

def divide(a, b)
  raise DivisionByZeroError, "Cannot divide by zero!" if b.zero?
  a / b
end

Boundaries

  • Encapsulate Third-Party Code: Create a thin wrapper or abstraction around external libraries or APIs you use. This isolates changes in those external components to one place. For example, if using a JSON parsing library, call it in one helper method (JsonParser.parse) instead of scattered JSON.parse calls everywhere — if you switch libraries, you update only that helper.
  • Boundary Tests (Learning Tests): Write tests to understand and pin down how external code works in your use case. Before integrating a new gem or API, a quick experiment or test cases can verify its behavior (and your understanding of it). These learning tests” give you confidence and act as documentation for the library’s usage.
  • Limit Interdependence: Minimize how much of your codebase knows about a third-party. Only a few classes/modules should interact directly with the external API; the rest of the code calls your abstraction. This prevents a ripple effect across your code if the external API changes.
  • Adapt to External Changes: When an external component must change (upgrade to a new version or swap out a library), having a well-defined boundary (adapter or gateway classes) means you only need to adjust that boundary code, not the entire codebase.
  • Defensive Coding at Boundaries: Be cautious with inputs and outputs at boundaries. Validate data coming from external sources and handle exceptions that external calls might throw. Assume the external code might not always behave or respond as expected, and code accordingly to protect your system.
# Before: using a library call directly in multiple places
data1 = JSON.parse(File.read("file1.json"))
data2 = JSON.parse(File.read("file2.json"))
# (If the JSON library changes, you'd have to update every call site)

# After: wrap external library in a single interface
module JsonUtil
  def self.load_file(path)
    JSON.parse(File.read(path))
  end
end

data1 = JsonUtil.load_file("file1.json")
data2 = JsonUtil.load_file("file2.json")
# (Now the JSON parsing logic is centralized and easier to maintain or change)

Unit Tests

  • Test One Thing: Each unit test should focus on one behavior or scenario. Write separate tests for distinct cases (rather than one huge test that does everything). This makes failures easier to diagnose and tests easier to understand.
  • Readable Test Names: Give tests descriptive names that clearly state what is being tested or the expected outcome. For example, test_valid_password_authenticates_user is better than test_user. In RSpec, use human-readable it "authenticates with a valid password" descriptions.
  • Clarity Over Cleverness: Keep test code simple and obvious. Avoid logic in tests (no complex calculations or conditional flows within a test) — tests should be straightforward sequences of setup, action, and assertion (Arrange-Act-Assert).
  • Assert Smartly: Use assertions that communicate intent. For instance, in Minitest or Test::Unit use assert_equal expected, actual with a clear message, or simply assert predicate for boolean results. In RSpec, use expressive matchers (e.g., expect(x).to be > 0). Each test should clearly state what it expects.
  • FAST & Independent: Tests should be fast to run and not depend on each other or external state (databases, network) unless absolutely necessary. Use test doubles (mocks/stubs) for isolating external dependencies. A slow or flaky test suite discourages running tests frequently, so aim for speedy, deterministic tests.
# Before
def test_user
  user = User.new("secret")
  assert_equal true, user.authenticate("secret")
end

# After
def test_authenticates_with_correct_password
  user = User.new("secret")
  assert user.authenticate("secret"), "Expected authentication to succeed with valid password"
end

Classes

  • Single Responsibility Principle: A class should have one reason to change — it should focus on a single responsibility or functionality. If you find yourself adding many unrelated methods or data to a class, consider breaking it up into multiple classes.
  • Keep Classes Small: Prefer more small classes to one do-it-all class. If a class grows beyond a few hundred lines or has dozens of methods, it’s likely doing too much. Smaller classes are easier to understand, test, and reuse.
  • High Cohesion: The elements of a class (methods and attributes) should be closely related. All methods should logically belong together. If you can remove a group of methods/fields to a new class and the original class still makes sense, that may improve cohesion in both.
  • Low Coupling: Reduce direct interdependence between classes. Where possible, depend on abstractions or use duck typing rather than concretions. For example, a class that needs to send notifications can accept any notifier” object that responds to #send, rather than specifically coupling to an SMTP email class. This makes code more flexible and testable.
  • Prefer Composition Over Inheritance: In Ruby, use modules or collaborator objects to share code instead of deep inheritance chains. Inheritance can make designs rigid or complex (and multiple inheritance isn’t directly supported, aside from mix-ins). Composition (having one class use instances of another) often leads to cleaner, more modular designs.
# Before: class with multiple responsibilities (order calculation + printing)
class Order
  def total
    # calculate total cost
  end

  def print_receipt
    # format and print order details
  end
end

# After: separate classes for separate responsibilities
class Order
  def total
    # calculate total cost
  end
end

class ReceiptPrinter
  def print(order)
    # format and print order details
  end
end

Systems

  • Design for Modularity: A well-structured system is divided into modules or layers that have clear responsibilities. For example, separate your domain logic from UI and persistence. Modular design (e.g., services, libraries, or Rails-style MVC) makes the system easier to understand and evolve in parts.
  • Separate Setup from Logic: Instantiate objects and wire dependencies in a dedicated place (or use dependency injection). This means your core logic isn’t littered with SomeClass.new calls. By separating configuration from use, you can change how components are connected without touching the business code.
  • Dependency Inversion: High-level components should not depend on low-level details; both should depend on abstractions. In practice, this means define interfaces or use duck types for interactions. For example, if a high-level service needs a logger, have it depend on an object that responds to log (interface) rather than a concrete FileLogger. This way, low-level implementations can be swapped easily.
  • Keep Frameworks at Bay: Don’t let frameworks or external tech choices dictate your core logic structure. Wrap framework-specific calls so that most of your code is framework-agnostic. For instance, in a Rails app, keep business logic out of controllers/models as much as possible (use plain Ruby objects) — this makes future migrations or reuse easier.
  • Evolve the Design: Continuously refactor and improve the system structure as it grows. Clean Code isn’t static — as new features are added, periodically revisit and simplify the architecture. This might mean extracting services, introducing design patterns, or breaking apart classes/modules when needed. Regular cleanup prevents the system architecture from decaying over time.
# Before: high-level code depends on a low-level class directly
class Lamp
  def turn_on
    puts "Lamp is now ON"
  end
end

class Switch
  def turn_on_lamp
    lamp = Lamp.new    # Switch is tightly coupled to Lamp
    lamp.turn_on
  end
end

# After: depend on an abstraction (any device with turn_on)
class Switch
  def initialize(device)
    @device = device   # device is a dependency injected from outside
  end

  def turn_on
    @device.turn_on    # works with any device that has a turn_on method
  end
end

lamp = Lamp.new
switch = Switch.new(lamp)
switch.turn_on

Emergent Design

Clean, simple design naturally emerges by following core principles. Kent Beck’s Four Rules of Simple Design summarize the goals:

  • Passes All Tests: The code works as intended — having a comprehensive test suite that always passes is the first priority. This gives you the safety net to refactor and improve design without fear.
  • Reveals Intent: The code clearly communicates its purpose. Another developer (or your future self) can easily understand why the code is written that way. Classes and methods are named to reflect their roles, and the code reads like well-written prose.”
  • No Duplication: Don’t repeat yourself. Any time you find duplicate logic or structure, refactor to unify it (e.g., extract a common method or class). Removing duplication not only reduces code size, but ensures there’s a single source of truth for each piece of knowledge in the system.
  • Minimal Classes and Methods: Achieve the desired functionality with the least complexity. That means no unnecessary classes, methods, or features are added just in case.” The design is as simple as it can be while still meeting requirements. This keeps the codebase lean and flexible.

(By consistently applying these rules and refactoring, a robust and simple design will emerge without heavy upfront design.)

Code Smells

Watch out for these common code smells — signs that code might need refactoring or improvement:

  • Duplicate Code: Same or very similar code appearing in multiple places (violates DRY). Solution: extract a method or unify the logic into one place.
  • Magic Numbers/Strings: Unexplained literal values in code (e.g., 60*60*24 with no context). They hinder understanding. Use named constants (e.g., SECONDS_PER_DAY = 86400) to give them meaning and make changes easier.
  • Long Method / Large Class: A method that’s too long or a class that has too many responsibilities is hard to read and maintain. Break them into smaller methods or classes (see Single Responsibility Principle). For methods, each should ideally fit on one screen and do one thing.
  • Long Parameter List: Passing more than a few parameters is a sign of trouble. It makes function calls hard to read and remember. Refactor by grouping parameters into an object, or split the function if it’s trying to do too much with all those inputs.
  • Flag Argument: Boolean parameters (e.g., process(true)) that select different behavior are a smell. They mean one function is doing two things. Instead, use two separate functions or subclasses for the two paths, which makes the code easier to follow.
  • Excessive Conditionals: Repeated if/else or case statements (especially on object types) suggest that polymorphism or strategy patterns could be used. E.g., instead of a giant case on a :type field, use subclasses or a hash mapping keys to procs to handle variations more cleanly.
  • Temporary Field: An attribute that is set only in certain cases or methods (often nil otherwise) indicates the class might be doing too much or has conditional logic that could be moved into smaller classes. Consider refactoring such that each class has all its fields always meaningful.
  • Excessive Comments: When you find a lot of comments explaining chunks of code, it often indicates the code isn’t clear enough. Rather than rely on comments, refactor the code to be self-explanatory. (Comments can mask bad code — it’s better to clean the code.)
  • Dead Code: Unused code (variables, methods, classes) that remains in the codebase is clutter. It confuses readers and may harbor bugs if ever accidentally invoked. Remove dead code promptly; source control remembers it if needed.
  • Inconsistent Naming or Coding Style: If similar concepts are named differently in different places (e.g., get_user vs fetch_customer for the same action) or style varies, it’s a smell that the code was not unified. Such inconsistency increases cognitive load. Renaming and cleaning up to a consistent style will make the code cleaner.
  • Hidden Coupling: Global state or singletons that cause implicit interactions between parts of the code (e.g., two classes relying on a global config) are smells. They make the code less modular and harder to test. Prefer explicit dependencies (pass needed objects as parameters) so interactions are clear.

Each of these smells has well-known refactorings or solutions. When you detect a smell, don’t ignore it — clean it. Small continuous improvements prevent big problems down the road, keeping your Ruby codebase easy to read, change, and maintain.


Tags
1 Pager

Date
May 3, 2025