Structural Patterns II: Decorator & Composite

Design Patterns

Chapter 5 · Structural Patterns II: Decorator & Composite

Two more ways of composing objects: adding behavior to an object at runtime without touching its class (Decorator), and letting a caller treat one object and a whole tree of objects through the exact same interface (Composite).

Decorator: Adding Behavior Without Modifying the Original

A coffee order needs an arbitrary combination of condiments, each adding its own cost. Decorator wraps the base object in layers, each layer adding one thing:

class SimpleCoffee: def cost(self): return 2.00 def description(self): return 'Coffee' class CondimentDecorator: def __init__(self, beverage): self._beverage = beverage class MilkDecorator(CondimentDecorator): def cost(self): return self._beverage.cost() + 0.50 def description(self): return self._beverage.description() + ', Milk' class SugarDecorator(CondimentDecorator): def cost(self): return self._beverage.cost() + 0.30 def description(self): return self._beverage.description() + ', Sugar'
Verified directly — cost and description accumulate correctly through the wrapping chain
SimpleCoffee(): 'Coffee', $2.00. MilkDecorator(coffee): 'Coffee, Milk', $2.50. SugarDecorator(MilkDecorator(coffee)): 'Coffee, Milk, Sugar', $2.80 — each layer correctly builds on the layer beneath it.
Verified directly — the base object stays unmodified, and separate chains stay independent
After building the Milk+Sugar chain above, the original base object still reports 'Coffee' and $2.00 — untouched. Wrapping that same base object in a completely separate chain, WhipDecorator(base), gives 'Coffee, Whip', $2.75 — genuinely different from the Milk+Sugar chain's $2.80, confirming the two decorator chains, built from the identical shared base, don't interfere with each other at all.

Composite: Treating One Object and a Whole Tree Identically

A file has a size. A directory's "size" is the sum of everything inside it — which might itself include other directories. Composite gives both the exact same interface:

class File: def __init__(self, name, size): self.name = name; self.size = size def get_size(self): return self.size class Directory: def __init__(self, name): self.name = name; self.children = [] def add(self, child): self.children.append(child) def get_size(self): return sum(child.get_size() for child in self.children)
Verified directly — a correct total across a genuinely nested tree
A tree three levels deep — project/ containing src/ (two files, 120+80), docs/ (one file, 45, plus a further-nested nested/ directory containing one more file, 30), and a top-level README.md (10): root.get_size() returns 285, matching a manual calculation (120+80+45+30+10) exactly.
Verified directly — the identical method call, no type-checking needed
Calling .get_size() on a single File (readme.txt, returns 5), on the entire root Directory (returns 285), and on the src Directory alone (returns 200) — the exact same one-line call, with no isinstance check or special-casing anywhere in the calling code, correctly handles a leaf, a shallow branch, and a deeply nested tree alike.

Where This Connects

This chapter's findingWhat it sets up
Decorator wrapping without modifying, verified two independent waysChapter 6's Proxy reuses the identical wrapping shape again, this time to control access rather than add behavior
Composite's verified uniform interface across a nested treeA concrete, working example of the same "recursion over a self-similar structure" idea pseudocode1's own Chapter 8 (divide and conquer) and Chapter 9 (recursion) already covered
Both patterns' zero-modification-to-existing-code disciplineClean Code, SOLID & Refactoring's own Open/Closed Principle, stated formally

Hands-On Exercises

Exercise 1

Using this chapter's own decorator classes, build a chain representing "Coffee, Whip, Milk, Sugar" (in that wrapping order) and compute its total cost by hand from each decorator's own added amount, then verify it against running the actual code.

📄 View solution
Exercise 2

Using this chapter's own File/Directory classes, add one more file (notes.txt, size 15) directly into the docs directory from this chapter's own worked example, and compute the new total root.get_size() both by hand and by running the code.

📄 View solution
Exercise 3

Using this chapter's own two patterns, explain why Decorator's own base-object-stays-unmodified guarantee and Composite's own uniform-interface guarantee are solving genuinely different problems, even though both patterns involve one object containing a reference to another.

📄 View solution

Chapter 5 Quick Reference

  • Decorator: wraps an object to add behavior at runtime, without modifying its class — verified accumulating correctly ($2.00 → $2.50 → $2.80), leaving the base object untouched, and keeping separate chains from the same base independent ($2.80 vs. $2.75)
  • Composite: a shared interface (get_size()) lets a caller treat a single leaf and a whole nested tree identically — verified: a 3-level-deep tree totaled 285, matching a manual calculation exactly, using the same one-line call at every level with zero type-checking
  • Next chapter: Structural Patterns III — Proxy and Flyweight