9.4 -- Structural Design Patterns: Quick Revision
Compact cheat sheet. Print-friendly.
How to use this material (instructions)
- Skim top-to-bottom in one pass before quizzes or interviews.
- If a row feels fuzzy -- open the matching lesson:
README.md -> 9.4.a...9.4.e.
- Deep practice --
9.4-Exercise-Questions.md.
- Polish phrasing --
9.4-Interview-Questions.md.
Master Comparison Table
| Adapter | Facade | Proxy | Decorator | Composite |
|---|
| One-liner | Translates interface A to B | Simplifies complex subsystem | Controls access to object | Adds behavior dynamically | Uniform tree structure |
| Wraps | 1 object (different interface) | Many objects (subsystem) | 1 object (same interface) | 1 object (same interface) | Many objects (tree) |
| Changes interface? | Yes (translates) | Yes (simplifies) | No | No | No |
| Adds behavior? | No | No | Sometimes (caching, logging) | Yes (primary purpose) | No (enables recursion) |
| Stackable? | Rarely | No | Sometimes | Yes (key feature) | Yes (tree nesting) |
| SOLID principle | Open/Closed | Single Responsibility | Open/Closed | Open/Closed | Open/Closed |
Pattern One-Liners (for interviews)
| Pattern | "Elevator pitch" |
|---|
| Adapter | "Makes incompatible interfaces work together by wrapping one in a translation layer." |
| Facade | "Provides a simplified interface to a complex subsystem so callers do not need to know the internals." |
| Proxy | "Places a surrogate in front of an object to control access -- same interface, added control." |
| Decorator | "Wraps an object to add behavior without modifying it -- like stacking middleware in Express." |
| Composite | "Treats individual objects and groups uniformly via a tree structure with recursive operations." |
When-to-Use Quick Guide
PROBLEM PATTERN
======================================= ==========
Third-party API has wrong shape --> ADAPTER
Multiple vendors, need one interface--> ADAPTER
Legacy system integration --> ADAPTER
Complex multi-step workflow --> FACADE
API Gateway for microservices --> FACADE
Reduce coupling to subsystem --> FACADE
Lazy loading expensive object --> PROXY (virtual)
Access control / permissions --> PROXY (protection)
Cache results transparently --> PROXY (caching)
Audit trail / request logging --> PROXY (logging)
Add logging without modifying code --> DECORATOR
Middleware pipeline (Express) --> DECORATOR
Combine optional behaviors freely --> DECORATOR
Retry, timeout, rate-limit wrappers --> DECORATOR
File system (files + folders) --> COMPOSITE
Menu with submenus --> COMPOSITE
Org chart (employees + departments) --> COMPOSITE
Nested permissions / groups --> COMPOSITE
Adapter -- Key Points
Client ----> Adapter ----> Adaptee
(our code) (translates) (third-party)
class StripeAdapter {
constructor(apiKey) {
this.stripe = new StripeSDK(apiKey);
}
async processPayment(amount, token) {
const result = await this.stripe.createCharge(
Math.round(amount * 100), 'usd', token
);
return { id: result.id, status: 'success', amount: result.amount / 100 };
}
}
- Object adapter (composition) preferred over class adapter (inheritance)
- Adapter does NOT add behavior -- only translates
- One adapter per vendor; client depends on the abstract interface
Facade -- Key Points
Client ----> Facade ----> SubsystemA
----> SubsystemB
----> SubsystemC
class EmailFacade {
constructor(smtpConfig) {
this.template = new TemplateEngine();
this.validator = new EmailValidator();
this.smtp = new SmtpClient(smtpConfig);
this.queue = new EmailQueue();
}
sendWelcomeEmail(user) {
this.validator.validate(user.email);
const { subject, body } = this.template.compile('welcome', { name: user.name });
this.smtp.send({ to: user.email, subject, body });
}
}
- Facade delegates, does NOT implement subsystem logic
- Clients can still bypass facade for fine-grained control
- Watch for God Object: keep each facade focused on one domain
Proxy -- Key Points
Client ----> Proxy ----> Real Object
(same interface)
| Type | What it does | Example |
|---|
| Virtual | Defers creation until first use | Image lazy loading |
| Protection | Checks permissions before forwarding | Document access control |
| Caching | Stores and returns cached results | Database query cache |
| Logging | Records every access | API request audit trail |
class ImageProxy {
constructor(filename) {
this.filename = filename;
this._real = null;
}
display() {
if (!this._real) this._real = new HeavyImage(this.filename);
this._real.display();
}
}
const validated = new Proxy(user, {
set(target, prop, value) {
if (prop === 'age' && (value < 0 || value > 150)) throw new Error('Invalid');
target[prop] = value;
return true;
}
});
Decorator -- Key Points
Client ----> DecoratorB ----> DecoratorA ----> Component
(each has same interface, each adds behavior)
function withLogging(fn) {
return async (...args) => {
console.log(`Calling ${fn.name}`, args);
const result = await fn(...args);
console.log(`Result:`, result);
return result;
};
}
function withCache(fn, ttl = 5000) {
const cache = new Map();
return async (...args) => {
const key = JSON.stringify(args);
const hit = cache.get(key);
if (hit && Date.now() - hit.time < ttl) return hit.data;
const result = await fn(...args);
cache.set(key, { data: result, time: Date.now() });
return result;
};
}
function withRetry(fn, max = 3) {
return async (...args) => {
for (let i = 1; i <= max; i++) {
try { return await fn(...args); }
catch (e) { if (i === max) throw e; }
}
};
}
const enhanced = withLogging(withCache(withRetry(fetchUser)));
- Express middleware = decorator pattern
- Order matters:
logging(cache(fn)) logs cache hits; cache(logging(fn)) does not
- Higher-order functions are the JS-native way to do decorators
Composite -- Key Points
[Root Composite]
/ | \
[Composite] [Leaf] [Composite]
/ \ / \
[Leaf] [Leaf] [Leaf] [Leaf]
class File {
constructor(name, size) { this.name = name; this.size = size; }
getSize() { return this.size; }
display(indent = '') { console.log(`${indent}- ${this.name} (${this.size}B)`); }
}
class Directory {
constructor(name) { this.name = name; this.children = []; }
add(child) { this.children.push(child); return this; }
getSize() {
return this.children.reduce((sum, c) => sum + c.getSize(), 0);
}
display(indent = '') {
console.log(`${indent}[${this.name}/]`);
this.children.forEach(c => c.display(indent + ' '));
}
}
- Three roles: Component (interface), Leaf (end node), Composite (container)
- All nodes respond to the same methods -- client never type-checks
- Power is in recursive operations: getSize, display, search, count, validate
Confused Pairs -- Quick Distinctions
| Comparison | Key difference |
|---|
| Adapter vs Facade | Adapter translates ONE interface; Facade simplifies a WHOLE subsystem |
| Proxy vs Decorator | Proxy controls ACCESS; Decorator adds BEHAVIOR |
| Adapter vs Proxy | Adapter changes the interface; Proxy keeps the SAME interface |
| Decorator vs Composite | Decorator wraps one object in layers; Composite builds a TREE of objects |
| Facade vs Proxy | Facade simplifies multiple objects; Proxy controls access to ONE object |
The Wrapper Family at a Glance
Pattern Wraps to... Interface Stackable?
========= ================= =========== ==========
ADAPTER TRANSLATE Different No
FACADE SIMPLIFY Different No
PROXY CONTROL ACCESS Same Sometimes
DECORATOR ADD BEHAVIOR Same Yes (key feature)
Real-World Implementations
| Pattern | JavaScript / Node.js example |
|---|
| Adapter | Database drivers (pg, mysql2) adapting vendor protocols to a common query interface |
| Facade | AWS SDK methods that orchestrate multiple API calls; Express app.listen() hiding HTTP server setup |
| Proxy | JavaScript Proxy object; Vue.js reactivity system; ORM lazy loading of relations |
| Decorator | Express/Koa middleware; TypeScript decorators (@Injectable, @Controller); higher-order components in React |
| Composite | React component tree; DOM (Document Object Model); file system APIs (fs recursive operations) |
<- Back to 9.4 -- Structural Design Patterns (README)