Skip to content

Patterns

When to use: a class or method has an interface about as complex as its implementation. How: find information it could hide; raise the interface level (one method for a whole computation instead of one per step); provide defaults; move rare features to separate methods so the common case is simple; merge closely tied shallow classes. Trade-offs: the implementation grows. Don’t merge unrelated code or build one class for the whole application.

When to use: the same knowledge (a file format, a protocol) lives in several modules. How: ask “How can I reorganize these classes so that this particular piece of knowledge only affects a single class?” Merge small, closely tied classes, or extract the knowledge into a new class with a simple interface. Trade-offs: an extracted class that exposes the knowledge in its interface just turns back-door leakage into interface leakage.

When to use: splitting an application into modules, especially read/process/write pipelines. How: list the pieces of knowledge the application needs, and give each module one or a few of them. Let order appear in the calling code, not in the module boundaries. Trade-offs: separate stages are fine if they truly use different information.

When to use: designing any new module. How: define the interface in the module’s own terms (text and positions, not keys and selections). Ask: what is the simplest interface covering current needs? In how many situations will each method be used? Is it easy to use for current needs? Trade-offs: too general (single-character operations) makes callers loop and runs slowly; too special leaks the caller’s concepts.

When to use: one class’s methods mostly forward to another class with the same signatures. How: ask which features each class owns. Then expose the lower class directly, redistribute features so each class is coherent, or merge the classes. Trade-offs: dispatchers and multiple implementations of one interface are legitimate duplication.

When to use: you’re about to wrap a class to add a feature. How: put general, related, or commonly used features in the base class; merge use-specific features into the use case; combine with an existing decorator; or build a stand-alone class. Trade-offs: sometimes a decorator really is the best option, but there is usually a better alternative.

When to use: data must reach deep code without being passed through every method, and globals would block multiple instances. How: one context per system instance holds configuration, shared subsystems, and counters. Store a reference in major objects, and pass it explicitly only to constructors. Trade-offs: it has most of the disadvantages of globals, can become a grab-bag, and may raise thread-safety issues. Keep its variables immutable where possible.

When to use: you’re tempted to throw an exception or add a configuration parameter because you’re unsure what to do. How: handle it inside the module if it is related to the module’s job, simplifies many callers, and shrinks the interface. Compute values such as retry intervals from measurements. If a parameter is unavoidable, give it an automatic default. Trade-offs: don’t pull down unrelated knowledge (a backspace method in a text class), and don’t hide information users really need.

When to use: a mechanism contains code for one particular use. How: keep the general mechanism alone in a lower layer and move special-purpose code up, near its use. Undo example: a general History class, action classes per kind of change, and grouping policy (fences) in the UI. Trade-offs: the rule is per mechanism; special-purpose undo code for text still belongs in the text class.

When to use: a method is long, or two methods only make sense together. How: split only to factor out a cleanly separable subtask (the parent keeps the interface) or to divide an interface that does unrelated things. Join methods that are shallow, duplicated, share intermediate data, or are conjoined. Trade-offs: never split into shallow methods callers must chain. Length alone is not a reason to split.

When to use: a method throws for a case callers would rather ignore. How: redefine its semantics so the case is normal. Tcl’s unset “should ensure that a variable no longer exists”; substring “returns the characters of the string (if any)” in the range; Unix marks an open file for deletion and frees it when the last process closes it. Trade-offs: only valid when callers don’t need to know about the case.

When to use: an exception can’t be defined away. How: mask it low in the stack (TCP resends, NFS retries); aggregate it high in the stack with one handler for a whole request loop, with each exception carrying its own message; or crash with a clear message for rare, unrecoverable errors (ckalloc for out of memory). Trade-offs: masking network errors entirely kills robustness; crashing is wrong where recovery is the product’s value; error promotion (RAMCloud) suits rare errors only.

When to use: a flag marks that something “doesn’t exist” and forces checks everywhere. How: make the thing always exist and represent the absent state as empty (a selection whose start equals its end), so normal code handles it. Trade-offs: the code must be written so the empty case truly needs no special handling.

When to use: every major interface, implementation, or decomposition decision. How: sketch radically different alternatives, list pros and cons (ease of use for callers first, then simplicity, generality, and efficiency), and pick, combine, or use shared weaknesses to invent a better option. Trade-offs: about an hour or two per class; more for larger modules, with a larger payback.

When to use: creating any new class. How: write the class comment, then the comments and signatures of the key methods, iterate, then comment the instance variables, then write the bodies. Write each new method’s comment before its body. Trade-offs: comments may need revising, but typing comments is only about 5% of development time.

When to use: documenting any method. How: one or two sentences on behavior as callers see it; each argument and the return value precisely, with constraints; side effects; exceptions; preconditions. No implementation details. Trade-offs: if the comment can’t be short and complete, fix the design (Hard to Describe).

When to use: a design decision affects several modules. How: put it where developers must go (the end of the Status enum listing every other update), or in a designNotes section with // See "Zombies" in designNotes. pointers. Trade-offs: central notes are far from the code and can go stale.

When to use: every bug fix or feature in existing code. How: ask whether the design is still right given the change; refactor so the result looks designed-in from the start; improve something each time; under deadline, find the cleanest option that fits and schedule the rest. Trade-offs: costs extra time now; repaid by faster later development.

When to use: measured, slow code with no fundamental fix available. How: write “the ideal”: the minimum code for the common case, in one method, with the most convenient data. Then find a clean design close to it, with one test at the start that diverts all special cases off the path. Measure before and after. Trade-offs: may need extra state (extraAppendBytes, a running totalLength). Back out changes that don’t measurably help, unless they simplify.