Key Principle
Over-specialization may be "the single greatest cause of complexity in software" (Chapter 6). General-purpose interfaces are paradoxically simpler: they replace many special-purpose methods with fewer general-purpose ones, producing less total code and no leakage of higher-level concepts into lower-level modules.
The guideline is "somewhat general-purpose": "The module's functionality should reflect your current needs, but its interface should not" (Chapter 6). Three diagnostic questions calibrate the balance:
- What is the simplest interface that covers all current needs?
- In how many situations will this method be used? Single-use methods are a red flag.
- Is this API easy to use for current needs? Too much generality forces excessive boilerplate.
Different layers should represent different abstractions. Specialization should be pushed up (into UI or application-specific layers) or down (into device drivers or platform-specific code), keeping middle layers clean and general. When a method does nothing but forward calls to another method with a similar signature, it is a pass-through method -- a red flag indicating the layer boundary is in the wrong place.
Why This Matters
When modules are designed around specific use cases rather than general capabilities, every new use case requires either a new method or contortions to repurpose an existing one. The interface grows, cognitive load climbs, and knowledge about higher-level concerns leaks into lower-level modules.
Without the "somewhat general-purpose" guideline, developers oscillate between two failure modes: over-specialized interfaces that proliferate methods, and over-generalized interfaces that impose boilerplate on every caller. The three diagnostic questions prevent both extremes.
Special-case conditionals are a particularly damaging form of specialization. Each conditional is a site for bugs and a branch that every reader must understand. Designing normal cases to handle edge conditions via degenerate representation eliminates these conditionals entirely.
Good Examples
Text editor API: Special-purpose API had
backspace(Cursor),delete(Cursor),deleteSelection(Selection)-- each method tied to a specific UI action. General-purpose API needed onlyinsert(Position, String),delete(Position, Position), andchangePosition(Position, int). Fewer methods, no UI-concept leakage into the text class. The general-purpose version was also shorter in total code. (Chapter 6)Empty selection as degenerate case: Instead of a "no selection" state requiring checks everywhere, represent no selection as an empty selection (start == end). Copy and delete handle it naturally with zero-length operations -- no special-case conditionals needed. Every eliminated conditional is one fewer site for bugs. (Chapter 6)
Layer separation: UI-specific behavior stays in UI classes (pushed up); device drivers implement a general-purpose interface using device-specific commands (pushed down). The middle layers stay clean, reusable, and stable across changes to either end. (Chapter 6)
Counterpoints
False abstraction: Omitting important details from an interface creates the illusion of simplicity. "It might appear simple, but in reality it isn't" (Chapter 4). File systems rightly omit block allocation but must expose flushing semantics for crash consistency. When details are important to callers, hiding them behind an interface creates obscurity, not simplicity. (Chapter 4)
Over-generality: If an API is too general, callers must write excessive boilerplate for common operations. The third diagnostic question -- "Is this API easy to use for current needs?" -- guards against this. Generality should simplify current usage, not complicate it. (Chapter 6)
Pass-through methods: Methods that do nothing but forward calls to another method with a similar signature add interface complexity without adding functionality -- the definition of a shallow module. They indicate that the layer boundary is in the wrong place and the abstractions on either side are not sufficiently different. (Chapter 6)
Key Quotes
"I now think that over-specialization may be the single greatest cause of complexity in software." -- John Ousterhout, Chapter 6
"The module's functionality should reflect your current needs, but its interface should not." -- John Ousterhout, Chapter 6
"It might appear simple, but in reality it isn't." -- John Ousterhout, Chapter 4
Rules of Thumb
- Design interfaces for general capabilities, not specific use cases; then implement only the functionality you need today
- If a method will only be used in one situation, suspect over-specialization
- Eliminate special cases by designing data representations where edge conditions are degenerate forms of the normal case
- Push specialization to the top layer (application-specific) or bottom layer (platform-specific); keep middle layers general
- When you find pass-through methods, reconsider whether the layer boundary belongs where you placed it
- Use the three diagnostic questions before finalizing any interface: simplest interface, number of use situations, ease of current use
Related References
- Deep Modules and Information Hiding - General-purpose interfaces are a primary technique for creating module depth
- Complexity: The Central Enemy - Interface design directly serves both elimination and encapsulation strategies
- Strategic vs. Tactical Programming - Good interface design requires the investment time that strategic programming budgets