Coffee, Wrapped (Decorator)
Stop breeding a class for every combination — wrap objects in layers and compose behavior at runtime.
The idea
A coffee shop prices drinks with inheritance: a Beverage base class, then one subclass per combination. DarkRoastWithMocha. DarkRoastWithMochaAndWhip. HouseBlendWithSoyAndMocha. Four drinks and a handful of condiments already put you at two dozen classes, and one new syrup owes the menu dozens more. Inheritance hands out behavior at compile time, once per class, so every combination needs its own class. Combinations grow multiplicatively. Classes should not.
The fix is almost embarrassingly simple. Instead of being a mocha dark roast, take a plain DarkRoast object and put it inside a Mocha object. The wrapper holds a reference to what it wraps, answers the same questions the wrapped thing answers, and adds its own bit on the way through: Mocha.cost() returns 0.20 plus the wrapped cost. Want whip too? Wrap the mocha in a Whip. The call travels inward to the plain drink and the answer accumulates on the way back out.
For that stacking to work, a wrapped coffee has to be usable anywhere a coffee is, so Mocha implements Beverage as well. This is the part that trips people up: the decorator inherits from the component not to get its behavior, but to get its type. Behavior comes from the object it wraps. Inheritance for type matching, composition for behavior.
You already use this every day. BufferedReader around a FileReader, a gzip reader around a buffered network connection, Python's io layers, Node's piped transform streams — every mainstream I/O library is a decorator chain, with a raw byte source in the middle and wrappers layering buffering, decoding and counting on top. Write one of those wrappers yourself and the pattern stops being a diagram.
The bench — 4 exercises
Collapse the Explosion
Refactor a Starbuzz pricing system from 24 hand-written combination subclasses into four drinks plus one small class per condiment, so that pricing becomes construction rather than class selection.
- Write out the starter yourself or take it from the repo: a Beverage base plus a couple dozen combo subclasses with copy-pasted hardcoded prices, and a menu.txt of orders it must price.
- Record the current price of every order in menu.txt as your regression baseline before changing anything.
- Introduce an abstract CondimentDecorator that shares Beverage's type and holds a wrapped Beverage field.
- Write four concrete drinks and one class per condiment (Mocha, Whip, Soy, SteamedMilk), then delete every combination subclass.
- Price an order that never had a subclass, such as Soy(Soy(Whip(Decaf))), and check the total by hand.
hint
If your decorator stores a DarkRoast field instead of a Beverage field, the second layer will not stack. The wrapped thing must be the abstract type — that is the whole trick.
hint
No pricing code should ask what type a drink is. If an instanceof or type switch survives the refactor, the wrapping is not doing the work yet.
DONE WHEN
· Every order in menu.txt prices to the same cent as the baseline you recorded
· Fewer than ten classes remain, and none has a combination name
· A double-soy order that had no subclass in the starter prices correctly
The Talking Onion
Add descriptions, a new condiment and size-aware pricing without editing a single existing class — open-closed made mechanical by freezing the files you already wrote.
- Snapshot a checksum of every file in your Starbuzz folder (a shasum listing committed to git works fine) — this is your freeze line.
- Make getDescription() compose through the chain so a nested order reads 'Espresso, Mocha, Mocha, Whip'.
- Add a Caramel condiment at 20 cents per layer, as a new file only.
- Give beverages a size of TALL, GRANDE or VENTI and make Soy cost 10, 15 or 20 cents based on the size of the drink it wraps.
- Re-run the checksum listing and diff it against the snapshot.
hint
getSize() on a decorator should be one line: return the wrapped beverage's size. Decorators add to some answers and pass others straight through.
hint
If size forces you to reopen an old file, do that edit before you freeze — deciding what belongs in the component's interface is the design work.
DONE WHEN
· The original menu still prices identically
· The checksum diff shows zero pre-existing files changed after the freeze
· The same drink with soy gives three different correct totals across the three sizes
Your Own Stream Decorator
Write two real stream wrappers and pipe an actual file through a three-deep chain that includes one of the standard library's own decorators — the moment the pattern stops being about coffee.
- Create a 40 KB mixed-case multi-line fixture file to run through the chain.
- Write LowercaseReader and LineNumberReader against your language's stream seam (extend FilterReader, wrap an io.TextIOBase, implement io.Reader holding an io.Reader, or subclass Transform).
- Build a small runner that composes a chain from a spec string, then run buffer,lower,linenum over the fixture into out.txt.
- Run the chain again as linenum,lower and diff the two outputs to see where ordering changes behavior.
- Wrap your decorator around a plain stdlib stream, and wrap a stdlib decorator around yours, to prove the type really matches.
hint
Read-side decorators are trickier than cost(): a read(n) call may hand you half a line, so keep a small internal buffer for the line-number prefix. This is exactly why BufferedReader exists.
hint
If it only works with your runner and not with an arbitrary stdlib stream, you wrote a string transform, not a decorator.
DONE WHEN
· out.txt is lowercased and line-numbered exactly as you expect, byte for byte
· The two chain orders produce outputs that differ only where the prefix casing changes
· Your decorator both accepts and is the standard stream type in both wrapping directions
Middleware Is Decorator
Recognize the pattern one more time in function shape by stacking logging, timing and auth wrappers around a toy in-process HTTP handler.
- Write a Handler interface taking a request and returning a response, plus a HelloHandler that counts how often it is called.
- Write three wrappers: Logging appends a line to a local file, Timing adds an elapsed header, Auth rejects tokenless requests without calling inward.
- Stack them as Logging(Timing(Auth(HelloHandler))) and fire a scripted mix of authorized and unauthorized requests.
- Re-stack with Auth outermost and compare the log file for the rejected requests.
hint
When a wrapper chooses not to call inward, you have edged past classic Decorator into interception — noticing that difference is the point of this one.
hint
Keep the server in-process with no sockets; nothing here needs a port.
DONE WHEN
· The inner handler's counter stays unchanged for every rejected request
· The log file's line order shows the onion being traversed in and back out
· Moving Auth outermost visibly changes which requests produce log lines
Go deeper (after the bench)
Read Head First Design Patterns Ch. 3 (pp. 79-108) now — the Starbuzz narrative is exercises 1 and 2, and its closing Java I/O section, where the authors write a LowerCaseInputStream, is the direct ancestor of exercise 3; compare their version to yours. If you want one more angle, Refactoring.Guru's free Decorator page has clean structure diagrams and an intent comparison against Adapter and Proxy, which you will meet in Modules 6 and 10.