Knowledge Guide
HomeOO & Low-Level Design

Structural

Step 6 in the OO & Low-Level Design path · 7 concepts · 0 problems

0 / 7 complete

📘 Learn Structural from zero

First principle: Structural patterns are about composition over inheritance — assembling objects and classes into larger structures while keeping them flexible and loosely coupled. Instead of building one giant rigid class, you snap together small pieces, each doing one job, connected by shared interfaces.

Analogy — a travel power adapter. Your laptop charger has European prongs; the US wall socket has a different shape. You can't reshape the wall and you can't reshape the charger, so you slot in a small travel adapter: one side fits the wall, the other fits your plug, and inside it just passes the electricity through. That is the Adapter pattern exactly — a thin translator between an interface the client expects and one an existing object provides.

Worked example. Your code logs through a clean interface: interface Logger { void log(String msg); }. You pull in a powerful third-party library whose only method is void writeEntry(int level, String text) — a different shape you can't edit. Write an object adapter that holds the library and delegates:

Now all your code keeps calling logger.log("hi"), unaware of writeEntry. The adapter absorbs the mismatch; swap libraries later → write a new adapter, change nothing else.

Key insight: every structural pattern is a deliberate indirection — you insert a small object between client and target so the two can evolve independently. The only question is why you indirect: to convert an interface (Adapter), to split two dimensions (Bridge), to unify a tree (Composite), to add behavior (Decorator), to simplify a subsystem (Facade), to share memory (Flyweight), or to control access (Proxy).

✨ Added by the guide to build intuition — not from the source course.

🎯 Guided practice

  1. Easy — Decorator: coffee pricing. Base Espresso costs $2. Customers add Milk (+$0.50) and Sugar (+$0.20), in any combination, possibly twice. Don't build EspressoWithMilkAndSugar subclasses.

    Reasoning: (1) Spot the trigger — responsibilities added at runtime in arbitrary combinations → Decorator. (2) Define the shared interface: interface Beverage { double cost(); String desc(); }. (3) Espresso implements Beverage returns 2.0 (the concrete component). (4) Each decorator holds a Beverage and is a Beverage: class Milk implements Beverage { Beverage inner; double cost(){ return inner.cost() + 0.50; } }. It delegates to inner, then adds its bit — this delegate-then-extend is the heart of the pattern. (5) Compose by wrapping: new Sugar(new Milk(new Espresso()))cost() = ((2.0)+0.50)+0.20 = 2.70. (6) Adding caramel = one new class, zero edits to existing ones (open/closed). Note it stays the same Beverage interface throughout — that's what distinguishes Decorator from Adapter, which converts to a different interface.

  2. Medium — Composite: filesystem size. Model files and folders so that totalSize() works identically whether called on a file or a deeply nested folder.

    Reasoning: (1) Trigger — a part-whole tree where the client should treat leaf and group uniformly → Composite. (2) One component interface so the client never branches on type: interface Node { int totalSize(); }. (3) Leaf = class File implements Node { int size; int totalSize(){ return size; } } — base case, returns its own bytes. (4) Composite = class Folder implements Node { List<Node> children; int totalSize(){ int s=0; for(Node c: children) s += c.totalSize(); return s; } } — recursive case, sums children via the same method. (5) Trace /root = Folder[ a.txt(100), Folder sub[ b.txt(50), c.txt(30) ] ]: root.totalSize() → 100 + sub.totalSize() → 100 + (50+30) = 180. (6) The client just calls node.totalSize(); recursion and tree shape are hidden inside Folder — uniform treatment is the payoff. (7) Design decision to name in an interview — GoF's transparency vs safety tradeoff for child operations like add(child): declare them only on Folder (type-safe, but the client may need casts and loses uniformity) or put them on Node and have File throw UnsupportedOperationException (transparent/uniform, but unsafe at runtime). Naming this tradeoff by its canonical terms signals FAANG-level depth.

✨ Added by the guide — work these before the full problem set.

Lessons in this topic