10.- Classes
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.