--- title: "Structural Design Patterns" description: "Reference for Adapter, Decorator, Facade, Proxy and other composition patterns" tags: [reference, design-patterns, architecture] --- # Structural Design Patterns Patterns that deal with object composition and relationships between entities, providing ways to assemble objects and classes into larger structures. ## Adapter ### Definition Converts the interface of a class into another interface clients expect, allowing incompatible interfaces to work together. ### When to Use - [x] Want to use an existing class with an incompatible interface - [x] Need to integrate third-party libraries with different interfaces - [x] Want to create a reusable class that cooperates with unrelated classes - [x] Legacy code must work with new systems ### TypeScript Signature ```typescript // Target interface (what client expects) interface Target { request(): string; } // Adaptee (existing incompatible class) class Adaptee { specificRequest(): string { return '.eetpadA eht fo roivaheb laicepS'; } } // Adapter (makes Adaptee compatible with Target) class Adapter implements Target { private adaptee: Adaptee; constructor(adaptee: Adaptee) { this.adaptee = adaptee; } public request(): string { const result = this.adaptee.specificRequest().split('').reverse().join(''); return `Adapter: ${result}`; } } // Client code function clientCode(target: Target) { console.log(target.request()); } // Usage const adaptee = new Adaptee(); const adapter = new Adapter(adaptee); clientCode(adapter); ``` ### Real-World Example: Third-Party Library Integration ```typescript // Third-party library (can't modify) class XMLDataProvider { getXMLData(): string { return '1'; } } // Your application expects JSON interface JSONDataProvider { getJSONData(): object; } // Adapter class XMLToJSONAdapter implements JSONDataProvider { constructor(private xmlProvider: XMLDataProvider) {} getJSONData(): object { const xml = this.xmlProvider.getXMLData(); // Convert XML to JSON (simplified) return { data: { item: '1' } }; } } // Usage const xmlProvider = new XMLDataProvider(); const adapter = new XMLToJSONAdapter(xmlProvider); const data = adapter.getJSONData(); ``` ### Detection Markers - Class implements target interface - Holds reference to adaptee - Delegates to adaptee with interface conversion - Names like `*Adapter`, `*Wrapper` ### Code Smells It Fixes - **Incompatible interfaces**: Makes legacy or third-party code compatible - **Interface proliferation**: Single adapter vs modifying multiple client calls ### Common Mistakes - **Two-way adapters**: Bidirectional conversion is complex; create two adapters - **Adapter chains**: Multiple adapters in sequence indicate design issues - **Overusing for new code**: Design compatible interfaces from the start --- ## Bridge ### Definition Decouples an abstraction from its implementation so the two can vary independently. ### When to Use - [x] Want to avoid permanent binding between abstraction and implementation - [x] Both abstractions and implementations should be extensible by subclassing - [x] Changes in implementation shouldn't affect clients - [x] Want to share implementation among multiple objects (Flyweight-like) ### TypeScript Signature ```typescript // Implementation interface interface Implementation { operationImpl(): string; } // Concrete implementations class ConcreteImplementationA implements Implementation { operationImpl(): string { return 'ConcreteImplementationA'; } } class ConcreteImplementationB implements Implementation { operationImpl(): string { return 'ConcreteImplementationB'; } } // Abstraction class Abstraction { constructor(protected implementation: Implementation) {} public operation(): string { return `Abstraction: ${this.implementation.operationImpl()}`; } } // Refined abstraction class ExtendedAbstraction extends Abstraction { public operation(): string { return `ExtendedAbstraction: ${this.implementation.operationImpl()}`; } } // Usage const implA = new ConcreteImplementationA(); const abstraction1 = new Abstraction(implA); console.log(abstraction1.operation()); const implB = new ConcreteImplementationB(); const abstraction2 = new ExtendedAbstraction(implB); console.log(abstraction2.operation()); ``` ### Real-World Example: UI Components with Multiple Renderers ```typescript // Implementation: Renderers interface Renderer { renderCircle(radius: number): string; renderSquare(side: number): string; } class VectorRenderer implements Renderer { renderCircle(radius: number): string { return `Drawing circle (vector) with radius ${radius}`; } renderSquare(side: number): string { return `Drawing square (vector) with side ${side}`; } } class RasterRenderer implements Renderer { renderCircle(radius: number): string { return `Drawing circle (pixels) with radius ${radius}`; } renderSquare(side: number): string { return `Drawing square (pixels) with side ${side}`; } } // Abstraction: Shapes abstract class Shape { constructor(protected renderer: Renderer) {} abstract draw(): string; } class Circle extends Shape { constructor(renderer: Renderer, private radius: number) { super(renderer); } draw(): string { return this.renderer.renderCircle(this.radius); } } class Square extends Shape { constructor(renderer: Renderer, private side: number) { super(renderer); } draw(): string { return this.renderer.renderSquare(this.side); } } // Usage: Can mix any shape with any renderer const vectorCircle = new Circle(new VectorRenderer(), 5); const rasterSquare = new Square(new RasterRenderer(), 10); ``` ### Detection Markers - Abstraction holds reference to implementation interface - Constructor injects implementation - Two parallel hierarchies (abstraction and implementation) ### Common Mistakes - **Confusion with Adapter**: Bridge is design-time; Adapter is runtime fix - **Over-engineering simple scenarios**: Use only when both hierarchies need to vary --- ## Composite ### Definition Composes objects into tree structures to represent part-whole hierarchies, letting clients treat individual objects and compositions uniformly. ### When to Use - [x] Want to represent part-whole hierarchies of objects - [x] Want clients to ignore difference between compositions and individual objects - [x] Tree structures are natural for the domain (file systems, UI components, org charts) ### TypeScript Signature ```typescript // Component interface interface Component { operation(): string; add?(component: Component): void; remove?(component: Component): void; getChild?(index: number): Component; } // Leaf (no children) class Leaf implements Component { constructor(private name: string) {} operation(): string { return this.name; } } // Composite (has children) class Composite implements Component { private children: Component[] = []; constructor(private name: string) {} add(component: Component): void { this.children.push(component); } remove(component: Component): void { const index = this.children.indexOf(component); if (index !== -1) { this.children.splice(index, 1); } } getChild(index: number): Component { return this.children[index]; } operation(): string { const results = this.children.map(child => child.operation()); return `${this.name}(${results.join(', ')})`; } } // Usage const tree = new Composite('root'); const branch1 = new Composite('branch1'); branch1.add(new Leaf('leaf1')); branch1.add(new Leaf('leaf2')); const branch2 = new Composite('branch2'); branch2.add(new Leaf('leaf3')); tree.add(branch1); tree.add(branch2); tree.add(new Leaf('leaf4')); console.log(tree.operation()); // Output: root(branch1(leaf1, leaf2), branch2(leaf3), leaf4) ``` ### Real-World Example: File System ```typescript interface FileSystemComponent { getName(): string; getSize(): number; print(indent: string): void; } class File implements FileSystemComponent { constructor(private name: string, private size: number) {} getName(): string { return this.name; } getSize(): number { return this.size; } print(indent: string): void { console.log(`${indent}📄 ${this.name} (${this.size} bytes)`); } } class Directory implements FileSystemComponent { private children: FileSystemComponent[] = []; constructor(private name: string) {} add(component: FileSystemComponent): void { this.children.push(component); } getName(): string { return this.name; } getSize(): number { return this.children.reduce((sum, child) => sum + child.getSize(), 0); } print(indent: string): void { console.log(`${indent}📁 ${this.name} (${this.getSize()} bytes)`); this.children.forEach(child => child.print(indent + ' ')); } } // Usage const root = new Directory('root'); const home = new Directory('home'); home.add(new File('photo.jpg', 2048)); home.add(new File('document.pdf', 4096)); const work = new Directory('work'); work.add(new File('report.docx', 8192)); root.add(home); root.add(work); root.print(''); ``` ### Detection Markers - Tree structure with uniform interface - Collection of children components - `add()`, `remove()`, `getChild()` methods - Recursive operation calls ### Code Smells It Fixes - **Type checking for composition vs leaf**: Uniform interface eliminates `instanceof` checks - **Different handling for parts vs wholes**: Clients treat both uniformly ### Common Mistakes - **Violating uniformity**: Leaf and Composite should have same interface - **Incorrect child management**: Not handling removal properly - **Deep recursion**: Can cause stack overflow on very deep trees --- ## Decorator ### Definition Attaches additional responsibilities to an object dynamically, providing a flexible alternative to subclassing for extending functionality. ### When to Use - [x] Need to add responsibilities to objects dynamically and transparently - [x] Responsibilities can be withdrawn - [x] Extension by subclassing is impractical (many possible combinations) - [x] Want to add features incrementally ### TypeScript Signature ```typescript // Component interface interface Component { operation(): string; } // Concrete component class ConcreteComponent implements Component { operation(): string { return 'ConcreteComponent'; } } // Base decorator abstract class Decorator implements Component { constructor(protected component: Component) {} operation(): string { return this.component.operation(); } } // Concrete decorators class DecoratorA extends Decorator { operation(): string { return `DecoratorA(${super.operation()})`; } } class DecoratorB extends Decorator { operation(): string { return `DecoratorB(${super.operation()})`; } } // Usage: Stack decorators const simple = new ConcreteComponent(); const decorated1 = new DecoratorA(simple); const decorated2 = new DecoratorB(decorated1); console.log(decorated2.operation()); // Output: DecoratorB(DecoratorA(ConcreteComponent)) ``` ### Stack-Native Alternatives **React - Higher-Order Components**: ```typescript // HOC decorator function withAuth

( Component: React.ComponentType

): React.ComponentType

{ return (props: P) => { const { user } = useAuth(); if (!user) return ; return ; }; } // Usage: Stack decorators const AuthenticatedProfile = withAuth(Profile); const AuthenticatedAdminProfile = withLogging(withAuth(Profile)); ``` **NestJS - Interceptors**: ```typescript @Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { console.log('Before...'); return next.handle().pipe( tap(() => console.log('After...')) ); } } // Apply decorator @UseInterceptors(LoggingInterceptor) @Controller('users') export class UsersController {} ``` ### Detection Markers - Implements same interface as wrapped object - Holds reference to wrapped object - Delegates to wrapped, adding behavior - Can be stacked ### Code Smells It Fixes - **Class explosion**: Avoid creating subclass for every feature combination - **Rigid feature addition**: Add/remove features dynamically ### Common Mistakes - **Order dependency**: DecoratorA(DecoratorB(x)) ≠ DecoratorB(DecoratorA(x)) - **Decorator explosion**: Too many small decorators can be hard to manage - **Breaking interface**: Decorator must maintain interface contract --- ## Facade ### Definition Provides a unified interface to a set of interfaces in a subsystem, making the subsystem easier to use. ### When to Use - [x] Want to provide a simple interface to a complex subsystem - [x] Many dependencies exist between clients and implementation classes - [x] Want to layer subsystems - [x] Need to decouple subsystem from clients ### TypeScript Signature ```typescript // Complex subsystem classes class SubsystemA { operationA(): string { return 'SubsystemA'; } } class SubsystemB { operationB(): string { return 'SubsystemB'; } } class SubsystemC { operationC(): string { return 'SubsystemC'; } } // Facade class Facade { private subsystemA: SubsystemA; private subsystemB: SubsystemB; private subsystemC: SubsystemC; constructor() { this.subsystemA = new SubsystemA(); this.subsystemB = new SubsystemB(); this.subsystemC = new SubsystemC(); } // Simplified interface public simpleOperation(): string { const resultA = this.subsystemA.operationA(); const resultB = this.subsystemB.operationB(); const resultC = this.subsystemC.operationC(); return `Facade coordinates: ${resultA}, ${resultB}, ${resultC}`; } } // Client code const facade = new Facade(); console.log(facade.simpleOperation()); // Instead of: // const a = new SubsystemA(); const b = new SubsystemB(); const c = new SubsystemC(); // a.operationA(); b.operationB(); c.operationC(); ``` ### Real-World Example: Payment Processing ```typescript // Complex subsystems class PaymentValidator { validate(amount: number, card: string): boolean { // Complex validation logic return amount > 0 && card.length === 16; } } class PaymentGateway { charge(amount: number, card: string): string { return `Charged $${amount} to ${card}`; } } class NotificationService { sendReceipt(email: string, transactionId: string): void { console.log(`Receipt sent to ${email}: ${transactionId}`); } } class TransactionLogger { log(transaction: string): void { console.log(`Logged: ${transaction}`); } } // Facade class PaymentFacade { private validator = new PaymentValidator(); private gateway = new PaymentGateway(); private notifications = new NotificationService(); private logger = new TransactionLogger(); processPayment(amount: number, card: string, email: string): boolean { // Simplified interface for complex process if (!this.validator.validate(amount, card)) { return false; } const result = this.gateway.charge(amount, card); this.logger.log(result); this.notifications.sendReceipt(email, result); return true; } } // Client code (simple!) const payment = new PaymentFacade(); payment.processPayment(100, '1234567890123456', 'user@example.com'); ``` ### Detection Markers - Class with multiple subsystem dependencies - Simple public methods coordinating subsystems - Named `*Facade`, `*API`, `*Service` ### Code Smells It Fixes - **Complex subsystem usage**: Clients don't need to know subsystem details - **Tight coupling**: Clients depend on facade, not many classes ### Common Mistakes - **God Facade**: Facade does too much; should coordinate, not contain logic - **Leaky abstraction**: Exposing subsystem details defeats the purpose --- ## Flyweight ### Definition Uses sharing to support large numbers of fine-grained objects efficiently by storing shared state externally. ### When to Use - [x] Application uses large number of objects - [x] Storage cost is high due to object quantity - [x] Most object state can be made extrinsic (externalized) - [x] Many groups of objects may be replaced by relatively few shared objects ### TypeScript Signature ```typescript // Flyweight class Flyweight { constructor(private sharedState: string) {} operation(uniqueState: string): void { console.log(`Flyweight: Shared (${this.sharedState}) and unique (${uniqueState}) state.`); } } // Flyweight factory class FlyweightFactory { private flyweights: Map = new Map(); constructor(initialFlyweights: string[][]) { for (const state of initialFlyweights) { this.flyweights.set(this.getKey(state), new Flyweight(state.join('_'))); } } private getKey(state: string[]): string { return state.join('_'); } getFlyweight(sharedState: string[]): Flyweight { const key = this.getKey(sharedState); if (!this.flyweights.has(key)) { console.log('Creating new flyweight'); this.flyweights.set(key, new Flyweight(key)); } else { console.log('Reusing existing flyweight'); } return this.flyweights.get(key)!; } listFlyweights(): void { console.log(`FlyweightFactory: ${this.flyweights.size} flyweights:`); for (const key of this.flyweights.keys()) { console.log(key); } } } // Usage const factory = new FlyweightFactory([ ['Chevrolet', 'Camaro2018', 'pink'], ['Mercedes Benz', 'C300', 'black'], ]); const flyweight1 = factory.getFlyweight(['Chevrolet', 'Camaro2018', 'pink']); flyweight1.operation('license-123'); const flyweight2 = factory.getFlyweight(['Chevrolet', 'Camaro2018', 'pink']); flyweight2.operation('license-456'); // Reuses same flyweight ``` ### Real-World Example: Text Editor Characters ```typescript // Flyweight: Character formatting (shared) class CharacterFormat { constructor( public font: string, public size: number, public color: string ) {} } // Flyweight factory class FormatFactory { private formats = new Map(); getFormat(font: string, size: number, color: string): CharacterFormat { const key = `${font}_${size}_${color}`; if (!this.formats.has(key)) { this.formats.set(key, new CharacterFormat(font, size, color)); } return this.formats.get(key)!; } } // Character with extrinsic state class Character { constructor( private char: string, private format: CharacterFormat // Shared flyweight ) {} render(position: number): string { return `'${this.char}' at ${position} (${this.format.font}, ${this.format.size}px, ${this.format.color})`; } } // Document const formatFactory = new FormatFactory(); const arial12Black = formatFactory.getFormat('Arial', 12, 'black'); const arial12Red = formatFactory.getFormat('Arial', 12, 'red'); // 10,000 characters, but only 2 format objects const characters: Character[] = []; for (let i = 0; i < 10000; i++) { const format = i % 2 === 0 ? arial12Black : arial12Red; characters.push(new Character('A', format)); } ``` ### Detection Markers - Factory managing pool of shared objects - Intrinsic (shared) vs extrinsic (unique) state separation - Map/cache of flyweights ### Common Mistakes - **Premature optimization**: Only use if memory is actually a problem - **Incorrect state separation**: Mixing intrinsic and extrinsic state --- ## Proxy ### Definition Provides a surrogate or placeholder for another object to control access to it. ### When to Use - [x] Lazy initialization (virtual proxy): Create expensive object only when needed - [x] Access control (protection proxy): Control access to original object - [x] Local representative of remote object (remote proxy) - [x] Logging, caching, or monitoring access ### TypeScript Signature ```typescript // Subject interface interface Subject { request(): void; } // Real subject class RealSubject implements Subject { request(): void { console.log('RealSubject: Handling request'); } } // Proxy class Proxy implements Subject { private realSubject: RealSubject | null = null; request(): void { // Access control if (this.checkAccess()) { // Lazy initialization if (!this.realSubject) { this.realSubject = new RealSubject(); } // Logging this.logAccess(); // Delegate to real subject this.realSubject.request(); } } private checkAccess(): boolean { console.log('Proxy: Checking access'); return true; } private logAccess(): void { console.log('Proxy: Logging access time'); } } // Usage const proxy = new Proxy(); proxy.request(); // Output: // Proxy: Checking access // Proxy: Logging access time // RealSubject: Handling request ``` ### Modern JavaScript Proxy ```typescript const target = { message: 'Hello', getValue() { return this.message; } }; const handler = { get(target: any, prop: string) { console.log(`Accessing property: ${prop}`); return target[prop]; }, set(target: any, prop: string, value: any) { console.log(`Setting property: ${prop} = ${value}`); target[prop] = value; return true; } }; const proxy = new Proxy(target, handler); console.log(proxy.message); // Logs: Accessing property: message proxy.message = 'World'; // Logs: Setting property: message = World ``` ### Detection Markers - Implements same interface as real subject - Holds reference to real subject - Controls access (checks, logging, caching) - Lazy initialization of real subject ### Common Mistakes - **Proxy chains**: Multiple proxies wrapping each other - **Performance overhead**: Every access goes through proxy - **Confusion with Decorator**: Proxy controls access; Decorator adds behavior --- ## Summary Table | Pattern | Complexity | Use Frequency | Main Benefit | |---------|------------|---------------|--------------| | Adapter | Low | High | Interface compatibility | | Bridge | High | Low | Decouple abstraction from implementation | | Composite | Medium | High | Uniform tree structure handling | | Decorator | Medium | High | Dynamic responsibility addition | | Facade | Low | Very High | Simplified subsystem interface | | Flyweight | High | Low | Memory optimization | | Proxy | Medium | Medium | Controlled access | ## Best Practices 1. **Adapter vs Bridge**: Adapter fixes incompatibility; Bridge designs flexibility 2. **Decorator vs Proxy**: Decorator adds features; Proxy controls access 3. **Facade simplicity**: Should coordinate, not contain business logic 4. **Composite uniformity**: Leaf and Composite must share interface 5. **Use native Proxy**: JavaScript `Proxy` object for dynamic property access ## References - *Design Patterns: Elements of Reusable Object-Oriented Software* (Gang of Four) - [MDN: Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) - [Refactoring Guru: Structural Patterns](https://refactoring.guru/design-patterns/structural-patterns)