Chapter 7: Different Layer, Different Abstraction
Core Idea
Section titled “Core Idea”In a well-designed system, each layer provides a different abstraction from the layers above and below it. Adjacent layers with similar abstractions, shown by pass-through methods, decorators, and pass-through variables, suggest that some layers don’t earn the complexity they add.
Frameworks Introduced
Section titled “Frameworks Introduced”- Different layer, different abstraction: if you follow one operation up and down through the layers, the abstraction should change with each method call.
- Examples: a file system goes from files (variable-length byte arrays) to a cache of fixed-size disk blocks to device drivers. TCP turns best-effort delivery of bounded-size packets into a reliable byte stream.
- Red flag: “adjacent layers with similar abstractions.”
- Fixing pass-through methods (Figure 7.1): first ask “Exactly which features and abstractions is each of these classes responsible for?” Then pick one of:
- (b) Expose the lower class: callers of C1 call C2 directly, and C1 keeps only its own feature.
- (c) Redistribute functionality: give each class a distinct, coherent set of features, so they no longer call each other.
- (d) Merge the classes, if they can’t be disentangled.
- When interface duplication is OK: each method with the same signature must contribute significant, distinct functionality.
- Dispatchers: choose which method should handle a request, such as a Web server matching URL rules.
- Multiple implementations of one interface, such as disk drivers. They reduce cognitive load because you learn the interface once. They are usually in the same layer and don’t call each other.
- Alternatives to a decorator: before creating one, ask:
- Could the new functionality go directly into the underlying class? Yes if it is general-purpose, logically related, or used by most users of that class.
- If it is specialized, could it merge with the use case instead?
- Could it merge with an existing decorator, giving one deeper decorator instead of several shallow ones?
- Does it need to wrap the class at all, or could it be a stand-alone class?
- Interface differs from implementation: the representation inside a class should differ from the abstraction in its interface. If they match, the class probably isn’t deep.
- Context object for pass-through variables: store all global state of the application in one context object per instance of the system, and keep a reference to it in the major objects.
Key Concepts
Section titled “Key Concepts”- Pass-through method: “does nothing except pass its arguments to another method, usually with the same API.” It shows an unclear division of responsibility.
- Red Flag: Pass-Through Method: it adds interface without functionality and creates a dependency on the callee’s signature.
- Decorator (wrapper): an object with the same API as the object it wraps, adding a feature. Examples:
BufferedInputStream,ScrollableWindow. Decorators tend to be shallow, full of boilerplate and pass-through methods. - Pass-through variable: a variable passed down a long chain of methods that don’t use it.
- Context object: holds configuration options, shared subsystems, performance counters, and anything else that would be a pass-through or global variable.
- Net gain rule: every design element (interface, argument, function, class, definition) adds complexity, so it must remove more complexity than it adds.
Mental Models
Section titled “Mental Models”- “The interface to a piece of functionality should be in the same class that implements the functionality.”
- Treat the gap between interface and representation as the value a class provides. A character-oriented text API over line-oriented storage hides all the line splitting and joining.
- Think of contexts as the least bad option, not a clean one. Variables in a context have most of the disadvantages of globals, the context can become “a huge grab-bag of data,” and it can raise thread-safety issues. Making context variables immutable is the best way to avoid those issues. “Unfortunately, I haven’t found a better solution than contexts.”
Anti-patterns
Section titled “Anti-patterns”- A class of pass-throughs: a student
TextDocumentclass had 13 pass-through methods out of 15 public methods. - A decorator for every small feature: an explosion of shallow classes, as in Java I/O.
- Line-oriented text API over line-oriented storage:
getLine/putLineforced UI code to split and join lines, and that nontrivial code was duplicated and scattered. - Global variables for shared state: they prevent two independent instances of a system in one process, which tests often need.
Code Examples
Section titled “Code Examples”public class TextDocument ... { private TextArea textArea; private TextDocumentListener listener; ... public Character getLastTypedCharacter() { return textArea.getLastTypedCharacter(); } public int getCursorOffset() { return textArea.getCursorOffset(); } public void insertString(String textToInsert, int offset) { textArea.insertString(textToInsert, offset); } public void willInsertString(String stringToInsert, int offset) { if (listener != null) { listener.willInsertString(this, stringToInsert, offset); } } ...}- What it demonstrates: pass-through methods. Only the last method does anything, and it only checks one variable. The student fixed it by moving methods between
TextDocument,TextArea, andTextDocumentListenerand collapsing the three classes into two with clearly different responsibilities.
Worked Example
Section titled “Worked Example”A certificate passed through a datacenter service (Figure 7.2). A command-line argument names certificates for secure communication. Only the low-level method m3 needs them, to call openSocket(cert, ...).
- (a) Pass-through variable.
certappears in the signatures ofmain,m1,m2, andm3, thoughm1andm2never use it. Adding certificate support later would mean changing many interfaces. - (b) A shared object. If
mainandm3already share an object (for example one holding network settings, shown with fieldsx,y,cert), storecertthere and callopenSocket(obj.cert, ...). But ask howm3gets that object; it may itself be a pass-through variable. - (c) A global variable.
m3reads a globalcert. No passing is needed, but you can no longer run two independent instances of the system in one process, which is often useful in testing. - (d) A context object (the author’s usual choice). One context per system instance holds
timeout,metrics,cert, and other global state. Objects that need it store a reference, and the context appears as an explicit argument only in constructors: a creating method takes the reference from its own object and passes it to the new object’s constructor.m3callsopenSocket(context.cert, ...). Adding a new variable touches only the context’s constructor and destructor, and tests can change configuration by setting context fields.
Key Takeaways
Section titled “Key Takeaways”- Check that each layer offers a different abstraction from its neighbors.
- When you find pass-through methods, decide which class owns each feature, then expose, redistribute, or merge.
- Allow duplicate signatures only when each method adds distinct functionality, as in dispatchers and multiple implementations.
- Try the four alternatives before writing a decorator.
- Make a class’s interface different from its internal representation.
- Replace pass-through variables with a context object, and keep context variables immutable where possible.
Connects To
Section titled “Connects To”- Ch 4: pass-throughs and decorators are shallow; Java I/O is the decorator example.
- Ch 6: the text class’s character-oriented interface.
- Ch 8: pulling line splitting down into the text class.
- Ch 9: when to merge or separate classes.
- Ch 10: a dispatcher is also where exceptions can be aggregated.
- Ch 19: design patterns such as decorators can be overused.