10.- Classes

18-02-2025 - Xavier Salvador

Chapter 10 moves the focus from functions to classes. Clean classes are small, cohesive, and embody a single responsibility. The same discipline applied to naming and sizing functions applies here at a higher level of abstraction.

Class Organisation

Following Java conventions, a well-organised class lists its sections in this order: public static constants, private static variables, private instance variables, public methods, and private helper methods placed immediately after the public method that calls them. This follows the stepdown rule: code reads top-to-bottom like a newspaper.

Encapsulation is preserved by default. Relaxing visibility (e.g., to protected or package-private) is only justified when tests demand it.

Classes Should Be Small

Small does not mean few lines — it means few responsibilities. The SuperDashboard class with five methods is still too large because it has two distinct reasons to change: it tracks version information and manages Swing components.

A class name is the first indicator of its size. If you cannot derive a concise name — if you resort to weasel words like Processor, Manager, or Super — the class likely has too many responsibilities. Similarly, a class description that requires the words “and”, “or”, or “but” is a red flag.

The Single Responsibility Principle (SRP)

A class should have one, and only one, reason to change.

Extracting version information from SuperDashboard into a dedicated Version class gives that responsibility a home with high reuse potential:

public class Version {
    public int getMajorVersionNumber() { ... }
    public int getMinorVersionNumber() { ... }
    public int getBuildNumber()        { ... }
}

Many developers hesitate to create many small classes, fearing complexity. In practice, a system with many small, focused classes is no harder to understand than a system with a few large ones — and significantly easier to change. The goal is tools organised into labelled drawers, not one large junk drawer.

Cohesion

A class is cohesive when each of its methods operates on most of its instance variables. A maximally cohesive class — one where every method uses every variable — is the ideal limit. The Stack class is a canonical example: push and pop use both topOfStack and elements; only size() uses just one.

public class Stack {
    private int topOfStack = 0;
    private List<Integer> elements = new LinkedList<>();

    public int size()  { return topOfStack; }
    public void push(int element) { topOfStack++; elements.add(element); }
    public int pop() throws PoppedWhenEmpty {
        if (topOfStack == 0) throw new PoppedWhenEmpty();
        int element = elements.get(--topOfStack);
        elements.remove(topOfStack);
        return element;
    }
}

When breaking a large function into smaller ones requires promoting local variables to instance variables, cohesion falls. That is a signal: those variables and the functions that share them belong in a separate class.

Maintaining Cohesion Results in Many Small Classes

Splitting a large function into smaller ones often reveals hidden classes. The refactored PrintPrimes example demonstrates this:

  • PrimePrinter — handles execution and environment concerns.
  • PrimeGenerator — encapsulates the algorithm to generate primes.
  • RowColumnPagePrinter — formats a list of numbers into paged columns.

Each class has one reason to change. Comprehension time for any one class drops to nearly zero. The change was achieved incrementally through a comprehensive test suite, one tiny step at a time.

Organising for Change

Every change to a class carries the risk of breaking something. The Sql class below violates SRP because it changes both when a new statement type is needed and when existing statement formatting changes:

public class Sql {
    public String create()       { ... }
    public String insert(...)    { ... }
    public String selectAll()    { ... }
    public String findByKey(...) { ... }
    public String select(...)    { ... }
    // ...
}

Refactoring each SQL operation into its own subclass of an abstract Sql eliminates both problems:

abstract public class Sql {
    abstract public String generate();
}

public class CreateSql  extends Sql { @Override public String generate() { ... } }
public class InsertSql  extends Sql { @Override public String generate() { ... } }
public class SelectSql  extends Sql { @Override public String generate() { ... } }
// Adding UpdateSql requires no changes to existing classes.

This respects the Open-Closed Principle (OCP): classes should be open for extension but closed for modification.

Isolating from Change — The Dependency Inversion Principle (DIP)

Depending on a concrete class couples your code to its implementation details. A Portfolio class that directly references TokyoStockExchange cannot be tested without hitting that external API.

Introducing a StockExchange interface decouples the two:

public interface StockExchange {
    Money currentPrice(String symbol);
}

public class Portfolio {
    private StockExchange exchange;
    public Portfolio(StockExchange exchange) { this.exchange = exchange; }
}

Tests can now inject a stub that returns fixed prices:

exchange = new FixedStockExchangeStub();
exchange.fix("MSFT", 100);
portfolio = new Portfolio(exchange);
portfolio.add(5, "MSFT");
assertEquals(500, portfolio.value());

The DIP states: depend on abstractions, not on concrete details. A system decoupled enough to be tested this way is also more flexible and easier to understand.

Key Rules

Principle What it means
SRP One class, one reason to change
Cohesion Methods share and operate on the same instance variables
OCP Extend by adding new classes; do not modify existing ones
DIP Depend on interfaces, not concrete implementations
Small size Favour many small, focused classes over a few large ones

Summary

Clean classes are small and focused, carrying a single responsibility that can be expressed in a concise name. High cohesion — methods that share the same instance variables — is the class-level counterpart of short, focused functions. The SRP, OCP, and DIP guide how classes change over time: new behaviour is added through extension, and volatile details are hidden behind abstractions. A system composed of many small, well-named classes is easier to understand, test, and change than a handful of monolithic ones.

results matching ""

    No results matching ""