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
.
a = 10
b = "John"
x = 15
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.
def process_data_and_save(input)
end
def process_data(input)
validate(input)
transformed = transform(input)
save_to_db(transformed)
end
def validate(input)
end
def transform(input)
end
def save_to_db(data)
end
- 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.
i += 1
i += 1
- 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.
def sum(a,b)
result=a+b
return result
end
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.
user_data = { name: "Alice", age: 25 }
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.
def divide(a, b)
if b == 0
puts "Cannot divide by zero"
return nil
end
a / b
end
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.
data1 = JSON.parse(File.read("file1.json"))
data2 = JSON.parse(File.read("file2.json"))
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")
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.
def test_user
user = User.new("secret")
assert_equal true, user.authenticate("secret")
end
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.
class Order
def total
end
def print_receipt
end
end
class Order
def total
end
end
class ReceiptPrinter
def print(order)
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.
class Lamp
def turn_on
puts "Lamp is now ON"
end
end
class Switch
def turn_on_lamp
lamp = Lamp.new
lamp.turn_on
end
end
class Switch
def initialize(device)
@device = device
end
def turn_on
@device.turn_on
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.
Date
May 3, 2025