Skip to main content
โšก Calmops

Structural Design Patterns in JavaScript

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

  1. Use Adapter for incompatible interfaces:

    // โœ… Good
    const adapter = new Adapter(legacySystem);
    
    // โŒ Bad
    const data = legacySystem.oldMethod();
    
  2. Use Composite for tree structures:

    // โœ… Good
    root.add(branch);
    branch.add(leaf);
    
    // โŒ Bad
    const tree = { children: [{ children: [] }] };
    
  3. Use Facade for complex subsystems:

    // โœ… Good
    const facade = new Facade();
    facade.complexOperation();
    
    // โŒ Bad
    subsystem1.operation();
    subsystem2.operation();
    subsystem3.operation();
    

Common Mistakes

  1. 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);
    
  2. 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

Next Steps

Comments