Structural Design Patterns in JavaScript
Structural patterns deal with object composition and relationships. This article covers Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy patterns.
Introduction
Structural patterns provide:
- Object composition
- Flexible relationships
- Simplified interfaces
- Reduced memory usage
- Enhanced functionality
Understanding these patterns helps you:
- Compose objects effectively
- Simplify complex systems
- Adapt incompatible interfaces
- Reduce memory footprint
- Enhance existing objects
Adapter Pattern
Basic Adapter
// โ
Good: Adapter pattern
class OldAPI {
getData() {
return { user_name: 'John', user_email: '[email protected]' };
}
}
class NewAPI {
getUser() {
throw new Error('Must implement getUser');
}
}
class APIAdapter extends NewAPI {
constructor(oldAPI) {
super();
this.oldAPI = oldAPI;
}
getUser() {
const data = this.oldAPI.getData();
return {
name: data.user_name,
email: data.user_email
};
}
}
// Usage
const oldAPI = new OldAPI();
const adapter = new APIAdapter(oldAPI);
console.log(adapter.getUser());
// { name: 'John', email: '[email protected]' }
Object Adapter
// โ
Good: Object adapter
class LegacyPaymentSystem {
processPayment(amount) {
console.log(`Processing payment: $${amount}`);
return true;
}
}
class ModernPaymentInterface {
pay(amount) {
throw new Error('Must implement pay');
}
}
class PaymentAdapter {
constructor(legacySystem) {
this.legacySystem = legacySystem;
}
pay(amount) {
return this.legacySystem.processPayment(amount);
}
}
// Usage
const legacy = new LegacyPaymentSystem();
const adapter = new PaymentAdapter(legacy);
adapter.pay(100); // Processing payment: $100
Bridge Pattern
// โ
Good: Bridge pattern
class Shape {
constructor(color) {
this.color = color;
}
draw() {
throw new Error('Must implement draw');
}
}
class Circle extends Shape {
draw() {
return `Drawing circle with ${this.color.getColor()}`;
}
}
class Rectangle extends Shape {
draw() {
return `Drawing rectangle with ${this.color.getColor()}`;
}
}
class Color {
getColor() {
throw new Error('Must implement getColor');
}
}
class RedColor extends Color {
getColor() {
return 'red';
}
}
class BlueColor extends Color {
getColor() {
return 'blue';
}
}
// Usage
const redCircle = new Circle(new RedColor());
console.log(redCircle.draw()); // Drawing circle with red
const blueRectangle = new Rectangle(new BlueColor());
console.log(blueRectangle.draw()); // Drawing rectangle with blue
Composite Pattern
Basic Composite
// โ
Good: Composite pattern
class Component {
constructor(name) {
this.name = name;
}
add(component) {
throw new Error('Must implement add');
}
remove(component) {
throw new Error('Must implement remove');
}
display(indent = '') {
throw new Error('Must implement display');
}
}
class Leaf extends Component {
display(indent = '') {
console.log(`${indent}${this.name}`);
}
}
class Composite extends Component {
constructor(name) {
super(name);
this.children = [];
}
add(component) {
this.children.push(component);
}
remove(component) {
this.children = this.children.filter(c => c !== component);
}
display(indent = '') {
console.log(`${indent}${this.name}`);
this.children.forEach(child => child.display(indent + ' '));
}
}
// Usage
const root = new Composite('root');
const branch1 = new Composite('branch1');
const branch2 = new Composite('branch2');
root.add(branch1);
root.add(branch2);
branch1.add(new Leaf('leaf1'));
branch1.add(new Leaf('leaf2'));
branch2.add(new Leaf('leaf3'));
root.display();
// root
// branch1
// leaf1
// leaf2
// branch2
// leaf3
File System Composite
// โ
Good: File system using composite
class FileSystemItem {
constructor(name) {
this.name = name;
}
getSize() {
throw new Error('Must implement getSize');
}
display(indent = '') {
throw new Error('Must implement display');
}
}
class File extends FileSystemItem {
constructor(name, size) {
super(name);
this.size = size;
}
getSize() {
return this.size;
}
display(indent = '') {
console.log(`${indent}๐ ${this.name} (${this.size}KB)`);
}
}
class Directory extends FileSystemItem {
constructor(name) {
super(name);
this.items = [];
}
add(item) {
this.items.push(item);
}
getSize() {
return this.items.reduce((sum, item) => sum + item.getSize(), 0);
}
display(indent = '') {
console.log(`${indent}๐ ${this.name}`);
this.items.forEach(item => item.display(indent + ' '));
}
}
// Usage
const root = new Directory('root');
const documents = new Directory('documents');
const images = new Directory('images');
root.add(documents);
root.add(images);
documents.add(new File('resume.pdf', 100));
documents.add(new File('cover_letter.doc', 50));
images.add(new File('photo.jpg', 500));
root.display();
console.log(`Total size: ${root.getSize()}KB`);
Decorator Pattern
Basic Decorator
// โ
Good: Decorator pattern
class Coffee {
cost() {
return 5;
}
description() {
return 'Coffee';
}
}
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost();
}
description() {
return this.coffee.description();
}
}
class Milk extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 1;
}
description() {
return this.coffee.description() + ', Milk';
}
}
class Sugar extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 0.5;
}
description() {
return this.coffee.description() + ', Sugar';
}
}
// Usage
let coffee = new Coffee();
console.log(coffee.description()); // Coffee
console.log(coffee.cost()); // 5
coffee = new Milk(coffee);
console.log(coffee.description()); // Coffee, Milk
console.log(coffee.cost()); // 6
coffee = new Sugar(coffee);
console.log(coffee.description()); // Coffee, Milk, Sugar
console.log(coffee.cost()); // 6.5
Function Decorator
// โ
Good: Function decorator
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with:`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
function withTiming(fn) {
return function(...args) {
const start = performance.now();
const result = fn(...args);
const end = performance.now();
console.log(`Execution time: ${end - start}ms`);
return result;
};
}
function add(a, b) {
return a + b;
}
const decoratedAdd = withTiming(withLogging(add));
decoratedAdd(2, 3);
// Calling add with: [2, 3]
// Result: 5
// Execution time: 0.123ms
Facade Pattern
// โ
Good: Facade pattern
class CPU {
freeze() {
console.log('CPU: Freezing');
}
jump(position) {
console.log(`CPU: Jumping to ${position}`);
}
execute() {
console.log('CPU: Executing');
}
}
class Memory {
load(position, data) {
console.log(`Memory: Loading ${data} at ${position}`);
}
}
class HardDrive {
read(lba, size) {
console.log(`HardDrive: Reading ${size} bytes from ${lba}`);
return 'data';
}
}
class ComputerFacade {
constructor() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
start() {
this.cpu.freeze();
this.memory.load(0, this.hardDrive.read(0, 1024));
this.cpu.jump(0);
this.cpu.execute();
}
}
// Usage
const computer = new ComputerFacade();
computer.start();
// CPU: Freezing
// Memory: Loading data at 0
// HardDrive: Reading 1024 bytes from 0
// CPU: Jumping to 0
// CPU: Executing
Flyweight Pattern
// โ
Good: Flyweight pattern
class TreeType {
constructor(name, color, texture) {
this.name = name;
this.color = color;
this.texture = texture;
}
draw(canvas, x, y) {
console.log(`Drawing ${this.name} at (${x}, ${y})`);
}
}
class TreeTypeFactory {
constructor() {
this.treeTypes = {};
}
getTreeType(name, color, texture) {
const key = `${name}_${color}_${texture}`;
if (!this.treeTypes[key]) {
this.treeTypes[key] = new TreeType(name, color, texture);
}
return this.treeTypes[key];
}
}
class Tree {
constructor(x, y, treeType) {
this.x = x;
this.y = y;
this.treeType = treeType;
}
draw(canvas) {
this.treeType.draw(canvas, this.x, this.y);
}
}
// Usage
const factory = new TreeTypeFactory();
const trees = [];
for (let i = 0; i < 1000; i++) {
const type = factory.getTreeType('Oak', 'green', 'rough');
trees.push(new Tree(Math.random() * 100, Math.random() * 100, type));
}
console.log(`Created 1000 trees with ${Object.keys(factory.treeTypes).length} unique types`);
// Created 1000 trees with 1 unique types
Proxy Pattern
Protection Proxy
// โ
Good: Protection proxy
class RealSubject {
request() {
return 'Real subject response';
}
}
class ProtectionProxy {
constructor(realSubject, password) {
this.realSubject = realSubject;
this.password = password;
}
request(password) {
if (password !== this.password) {
throw new Error('Access denied');
}
return this.realSubject.request();
}
}
// Usage
const subject = new RealSubject();
const proxy = new ProtectionProxy(subject, 'secret');
console.log(proxy.request('secret')); // Real subject response
console.log(proxy.request('wrong')); // Error: Access denied
Virtual Proxy
// โ
Good: Virtual proxy for lazy loading
class ExpensiveObject {
constructor() {
console.log('Creating expensive object');
this.data = 'Important data';
}
getData() {
return this.data;
}
}
class VirtualProxy {
constructor() {
this.realObject = null;
}
getData() {
if (!this.realObject) {
this.realObject = new ExpensiveObject();
}
return this.realObject.getData();
}
}
// Usage
const proxy = new VirtualProxy();
console.log('Proxy created');
console.log(proxy.getData()); // Creating expensive object, Important data
console.log(proxy.getData()); // Important data (no creation)
Best Practices
-
Use Adapter for incompatible interfaces:
// โ Good const adapter = new Adapter(legacySystem); // โ Bad const data = legacySystem.oldMethod(); -
Use Composite for tree structures:
// โ Good root.add(branch); branch.add(leaf); // โ Bad const tree = { children: [{ children: [] }] }; -
Use Facade for complex subsystems:
// โ Good const facade = new Facade(); facade.complexOperation(); // โ Bad subsystem1.operation(); subsystem2.operation(); subsystem3.operation();
Common Mistakes
-
Overusing Decorator:
// โ Bad - too many decorators const obj = new D1(new D2(new D3(new D4(original)))); // โ Good - use composition const obj = compose(original, d1, d2, d3); -
Proxy without clear purpose:
// โ Bad - unnecessary proxy const proxy = new Proxy(obj, {}); // โ Good - proxy with purpose const proxy = new Proxy(obj, { get: (t, p) => { } });
Summary
Structural patterns organize object relationships. Key takeaways:
- Adapter: Incompatible interfaces
- Bridge: Abstraction and implementation
- Composite: Tree structures
- Decorator: Add functionality
- Facade: Simplify complex systems
- Flyweight: Reduce memory
- Proxy: Control access
- Improves flexibility
- Reduces coupling
- Enhances functionality
Related Resources
- Structural Patterns - Refactoring Guru
- Composite Pattern - MDN
- Decorator Pattern - Wikipedia
- Facade Pattern - Wikipedia
- Design Patterns in JavaScript
Next Steps
- Learn about Behavioral Design Patterns
- Explore SOLID Principles
- Study Creational Patterns
- Practice structural patterns
- Build reusable adapters
Comments