Development Blog

Technical insights, design patterns, and best practices

Software Design Patterns Quick Reference

Design patterns are reusable solutions to common problems in software design. They represent best practices evolved over time and provide a shared vocabulary for developers to communicate complex ideas efficiently.

Creational Patterns

These patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.

Singleton Pattern

Ensures a class has only one instance and provides a global point of access to it.

When to Use:

  • Database connections
  • Configuration managers
  • Logger instances
  • Cache management
JavaScript Example
class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }
    this.connection = null;
    Database.instance = this;
  }

  connect() {
    if (!this.connection) {
      this.connection = 'Connected to database';
    }
    return this.connection;
  }
}

// Usage
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true

Factory Pattern

Creates objects without specifying the exact class of object that will be created.

When to Use:

  • When the exact type of object isn't known until runtime
  • Complex object creation logic
  • Creating different objects based on conditions
  • Centralizing object creation
JavaScript Example
class Car {
  constructor(options) {
    this.doors = options.doors || 4;
    this.color = options.color || 'silver';
  }
}

class Truck {
  constructor(options) {
    this.wheels = options.wheels || 6;
    this.color = options.color || 'red';
  }
}

class VehicleFactory {
  createVehicle(type, options) {
    switch(type) {
      case 'car':
        return new Car(options);
      case 'truck':
        return new Truck(options);
      default:
        throw new Error('Unknown vehicle type');
    }
  }
}

// Usage
const factory = new VehicleFactory();
const myCar = factory.createVehicle('car', { color: 'blue' });
const myTruck = factory.createVehicle('truck', { wheels: 8 });

Builder Pattern

Separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

When to Use:

  • Objects with many optional parameters
  • Step-by-step construction processes
  • Immutable objects
  • Complex object initialization
JavaScript Example
class HttpRequest {
  constructor(builder) {
    this.url = builder.url;
    this.method = builder.method;
    this.headers = builder.headers;
    this.body = builder.body;
  }
}

class HttpRequestBuilder {
  constructor(url) {
    this.url = url;
    this.method = 'GET';
    this.headers = {};
    this.body = null;
  }

  setMethod(method) {
    this.method = method;
    return this;
  }

  addHeader(key, value) {
    this.headers[key] = value;
    return this;
  }

  setBody(body) {
    this.body = body;
    return this;
  }

  build() {
    return new HttpRequest(this);
  }
}

// Usage
const request = new HttpRequestBuilder('https://api.example.com/users')
  .setMethod('POST')
  .addHeader('Content-Type', 'application/json')
  .setBody({ name: 'John' })
  .build();

Prototype Pattern

Creates new objects by copying an existing object (prototype) rather than creating new instances from scratch.

When to Use:

  • Expensive object creation operations
  • Complex object initialization
  • Avoiding subclasses for object creation
  • Dynamic object composition
JavaScript Example
class CarPrototype {
  constructor() {
    this.wheels = 4;
    this.engine = 'V6';
    this.color = 'silver';
  }

  clone() {
    return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
  }
}

// Usage
const protoCar = new CarPrototype();
protoCar.color = 'blue';

const car1 = protoCar.clone();
const car2 = protoCar.clone();
car2.color = 'red';

console.log(car1.color); // blue
console.log(car2.color); // red

Abstract Factory Pattern

Provides an interface for creating families of related or dependent objects without specifying their concrete classes.

When to Use:

  • System needs to be independent of product creation
  • Related product families
  • Cross-platform applications
  • UI themes and skins
JavaScript Example
// Abstract Products
class Button {
  render() { throw new Error('Must implement render'); }
}

class Checkbox {
  render() { throw new Error('Must implement render'); }
}

// Concrete Products - Windows
class WindowsButton extends Button {
  render() { return 'Render Windows Button'; }
}

class WindowsCheckbox extends Checkbox {
  render() { return 'Render Windows Checkbox'; }
}

// Concrete Products - Mac
class MacButton extends Button {
  render() { return 'Render Mac Button'; }
}

class MacCheckbox extends Checkbox {
  render() { return 'Render Mac Checkbox'; }
}

// Abstract Factory
class GUIFactory {
  createButton() { throw new Error('Must implement createButton'); }
  createCheckbox() { throw new Error('Must implement createCheckbox'); }
}

// Concrete Factories
class WindowsFactory extends GUIFactory {
  createButton() { return new WindowsButton(); }
  createCheckbox() { return new WindowsCheckbox(); }
}

class MacFactory extends GUIFactory {
  createButton() { return new MacButton(); }
  createCheckbox() { return new MacCheckbox(); }
}

// Usage
const os = 'Windows';
const factory = os === 'Windows' ? new WindowsFactory() : new MacFactory();
const button = factory.createButton();
console.log(button.render()); // "Render Windows Button"

Structural Patterns

These patterns deal with object composition, creating relationships between objects to form larger structures.

Adapter Pattern

Allows incompatible interfaces to work together by wrapping an object with an adapter to make it compatible with another class.

When to Use:

  • Integrating third-party libraries
  • Legacy code integration
  • Interface compatibility
  • API versioning
JavaScript Example
// Old interface
class OldCalculator {
  operations(term1, term2, operation) {
    switch (operation) {
      case 'add': return term1 + term2;
      case 'sub': return term1 - term2;
    }
  }
}

// New interface
class NewCalculator {
  add(term1, term2) {
    return term1 + term2;
  }
  subtract(term1, term2) {
    return term1 - term2;
  }
}

// Adapter
class CalculatorAdapter {
  constructor() {
    this.newCalc = new NewCalculator();
  }

  operations(term1, term2, operation) {
    switch (operation) {
      case 'add':
        return this.newCalc.add(term1, term2);
      case 'sub':
        return this.newCalc.subtract(term1, term2);
    }
  }
}

// Usage
const adapter = new CalculatorAdapter();
console.log(adapter.operations(5, 3, 'add')); // 8

Decorator Pattern

Adds new functionality to an existing object without altering its structure.

When to Use:

  • Adding features to objects dynamically
  • Extending functionality without inheritance
  • Multiple combinations of features
  • Middleware implementations
JavaScript Example
class Coffee {
  cost() {
    return 5;
  }
  description() {
    return 'Simple coffee';
  }
}

class MilkDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  cost() {
    return this.coffee.cost() + 2;
  }
  description() {
    return this.coffee.description() + ', milk';
  }
}

class SugarDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  cost() {
    return this.coffee.cost() + 1;
  }
  description() {
    return this.coffee.description() + ', sugar';
  }
}

// Usage
let myCoffee = new Coffee();
myCoffee = new MilkDecorator(myCoffee);
myCoffee = new SugarDecorator(myCoffee);
console.log(myCoffee.description()); // "Simple coffee, milk, sugar"
console.log(myCoffee.cost()); // 8

Facade Pattern

Provides a simplified interface to a complex subsystem.

When to Use:

  • Simplifying complex systems
  • Reducing dependencies
  • Layered architecture
  • API abstraction
JavaScript Example
// Complex subsystems
class CPU {
  freeze() { console.log('CPU frozen'); }
  jump(position) { console.log(`Jumping to ${position}`); }
  execute() { console.log('Executing'); }
}

class Memory {
  load(position, data) { 
    console.log(`Loading ${data} at ${position}`); 
  }
}

class HardDrive {
  read(lba, size) { 
    return 'boot data'; 
  }
}

// Facade
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(); // Simple interface to complex startup

Composite Pattern

Composes objects into tree structures to represent part-whole hierarchies, allowing clients to treat individual objects and compositions uniformly.

When to Use:

  • Tree structures (file systems, DOM)
  • Hierarchical data
  • Menu systems
  • Organization charts
JavaScript Example
// Component
class FileSystemItem {
  constructor(name) {
    this.name = name;
  }
  getSize() { throw new Error('Must implement getSize'); }
}

// Leaf
class File extends FileSystemItem {
  constructor(name, size) {
    super(name);
    this.size = size;
  }
  getSize() {
    return this.size;
  }
}

// Composite
class Folder extends FileSystemItem {
  constructor(name) {
    super(name);
    this.children = [];
  }

  add(item) {
    this.children.push(item);
  }

  getSize() {
    return this.children.reduce((total, child) => total + child.getSize(), 0);
  }
}

// Usage
const root = new Folder('root');
const documents = new Folder('documents');
const photos = new Folder('photos');

documents.add(new File('resume.pdf', 100));
documents.add(new File('cover.docx', 50));
photos.add(new File('photo1.jpg', 200));
photos.add(new File('photo2.jpg', 150));

root.add(documents);
root.add(photos);

console.log(root.getSize()); // 500

Proxy Pattern

Provides a surrogate or placeholder for another object to control access to it.

When to Use:

  • Lazy initialization (virtual proxy)
  • Access control (protection proxy)
  • Remote object access (remote proxy)
  • Caching (smart reference)
JavaScript Example
// Real Subject
class Image {
  constructor(filename) {
    this.filename = filename;
    this.loadFromDisk();
  }

  loadFromDisk() {
    console.log(`Loading ${this.filename}`);
  }

  display() {
    console.log(`Displaying ${this.filename}`);
  }
}

// Proxy
class ImageProxy {
  constructor(filename) {
    this.filename = filename;
    this.image = null;
  }

  display() {
    if (!this.image) {
      this.image = new Image(this.filename);
    }
    this.image.display();
  }
}

// Usage
const image1 = new ImageProxy('photo1.jpg');
// Image not loaded yet
image1.display(); // Loads and displays
image1.display(); // Just displays (already loaded)

Bridge Pattern

Decouples an abstraction from its implementation so that the two can vary independently.

When to Use:

  • Avoiding permanent binding between abstraction and implementation
  • Platform-independent code
  • Runtime implementation switching
  • Reducing class explosion from inheritance
JavaScript Example
// Implementation
class Renderer {
  renderCircle(radius) { throw new Error('Must implement'); }
}

class VectorRenderer extends Renderer {
  renderCircle(radius) {
    console.log(`Drawing circle with radius ${radius} using vectors`);
  }
}

class RasterRenderer extends Renderer {
  renderCircle(radius) {
    console.log(`Drawing circle with radius ${radius} using pixels`);
  }
}

// Abstraction
class Shape {
  constructor(renderer) {
    this.renderer = renderer;
  }
  draw() { throw new Error('Must implement'); }
}

class Circle extends Shape {
  constructor(renderer, radius) {
    super(renderer);
    this.radius = radius;
  }

  draw() {
    this.renderer.renderCircle(this.radius);
  }
}

// Usage
const vectorCircle = new Circle(new VectorRenderer(), 5);
vectorCircle.draw(); // "Drawing circle with radius 5 using vectors"

const rasterCircle = new Circle(new RasterRenderer(), 5);
rasterCircle.draw(); // "Drawing circle with radius 5 using pixels"

Flyweight Pattern

Minimizes memory usage by sharing as much data as possible with similar objects.

When to Use:

  • Large numbers of similar objects
  • Memory optimization
  • Immutable shared state
  • Text rendering, particle systems
JavaScript Example
// Flyweight
class TreeType {
  constructor(name, color, texture) {
    this.name = name;
    this.color = color;
    this.texture = texture;
  }

  draw(x, y) {
    console.log(`Drawing ${this.name} tree at (${x}, ${y})`);
  }
}

// Flyweight Factory
class TreeFactory {
  constructor() {
    this.treeTypes = {};
  }

  getTreeType(name, color, texture) {
    const key = `${name}-${color}-${texture}`;
    if (!this.treeTypes[key]) {
      this.treeTypes[key] = new TreeType(name, color, texture);
      console.log(`Creating new tree type: ${key}`);
    }
    return this.treeTypes[key];
  }
}

// Context
class Tree {
  constructor(x, y, type) {
    this.x = x;
    this.y = y;
    this.type = type;
  }

  draw() {
    this.type.draw(this.x, this.y);
  }
}

// Usage
const factory = new TreeFactory();
const trees = [];

trees.push(new Tree(1, 2, factory.getTreeType('Oak', 'green', 'oak.png')));
trees.push(new Tree(5, 8, factory.getTreeType('Oak', 'green', 'oak.png')));
trees.push(new Tree(3, 4, factory.getTreeType('Pine', 'dark-green', 'pine.png')));

trees.forEach(tree => tree.draw());
// Only 2 TreeType objects created despite 3 trees

Behavioral Patterns

These patterns are concerned with algorithms and the assignment of responsibilities between objects.

Observer Pattern

Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically.

When to Use:

  • Event handling systems
  • State management (Redux, MobX)
  • Publish/Subscribe systems
  • Real-time updates
JavaScript Example
class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }

  update(data) {
    console.log(`${this.name} received: ${data}`);
  }
}

// Usage
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('Hello observers!');
// Output:
// Observer 1 received: Hello observers!
// Observer 2 received: Hello observers!

Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

When to Use:

  • Multiple algorithms for a task
  • Conditional logic replacement
  • Payment processing systems
  • Sorting algorithms
JavaScript Example
// Strategies
class CreditCardStrategy {
  pay(amount) {
    console.log(`Paid ${amount} using Credit Card`);
  }
}

class PayPalStrategy {
  pay(amount) {
    console.log(`Paid ${amount} using PayPal`);
  }
}

class CryptoStrategy {
  pay(amount) {
    console.log(`Paid ${amount} using Cryptocurrency`);
  }
}

// Context
class PaymentContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  executePayment(amount) {
    this.strategy.pay(amount);
  }
}

// Usage
const payment = new PaymentContext(new CreditCardStrategy());
payment.executePayment(100); // Paid 100 using Credit Card

payment.setStrategy(new PayPalStrategy());
payment.executePayment(50); // Paid 50 using PayPal

Command Pattern

Encapsulates a request as an object, allowing you to parameterize clients with different requests, queue requests, and support undoable operations.

When to Use:

  • Undo/Redo functionality
  • Task queuing
  • Transaction systems
  • Macro recording
JavaScript Example
// Receiver
class TextEditor {
  constructor() {
    this.text = '';
  }

  write(text) {
    this.text += text;
  }

  delete(length) {
    this.text = this.text.slice(0, -length);
  }

  getText() {
    return this.text;
  }
}

// Commands
class WriteCommand {
  constructor(editor, text) {
    this.editor = editor;
    this.text = text;
  }

  execute() {
    this.editor.write(this.text);
  }

  undo() {
    this.editor.delete(this.text.length);
  }
}

// Invoker
class CommandManager {
  constructor() {
    this.history = [];
  }

  execute(command) {
    command.execute();
    this.history.push(command);
  }

  undo() {
    const command = this.history.pop();
    if (command) {
      command.undo();
    }
  }
}

// Usage
const editor = new TextEditor();
const manager = new CommandManager();

manager.execute(new WriteCommand(editor, 'Hello '));
manager.execute(new WriteCommand(editor, 'World!'));
console.log(editor.getText()); // "Hello World!"

manager.undo();
console.log(editor.getText()); // "Hello "

Iterator Pattern

Provides a way to access elements of a collection sequentially without exposing its underlying representation.

When to Use:

  • Traversing complex data structures
  • Multiple traversal algorithms
  • Hiding collection implementation
  • Custom iteration logic
JavaScript Example
class BookCollection {
  constructor() {
    this.books = [];
  }

  addBook(book) {
    this.books.push(book);
  }

  [Symbol.iterator]() {
    let index = 0;
    const books = this.books;

    return {
      next() {
        if (index < books.length) {
          return { value: books[index++], done: false };
        }
        return { done: true };
      }
    };
  }
}

// Usage
const collection = new BookCollection();
collection.addBook({ title: '1984', author: 'Orwell' });
collection.addBook({ title: 'Brave New World', author: 'Huxley' });

for (const book of collection) {
  console.log(`${book.title} by ${book.author}`);
}
// Output:
// 1984 by Orwell
// Brave New World by Huxley

Mediator Pattern

Defines an object that encapsulates how a set of objects interact, promoting loose coupling.

When to Use:

  • Complex object interactions
  • Reducing object dependencies
  • Chat rooms, air traffic control
  • Component communication
JavaScript Example
// Mediator
class ChatRoom {
  constructor() {
    this.users = {};
  }

  register(user) {
    this.users[user.name] = user;
    user.chatRoom = this;
  }

  send(message, from, to) {
    if (to) {
      to.receive(message, from);
    } else {
      Object.values(this.users).forEach(user => {
        if (user !== from) {
          user.receive(message, from);
        }
      });
    }
  }
}

// Colleague
class User {
  constructor(name) {
    this.name = name;
    this.chatRoom = null;
  }

  send(message, to) {
    this.chatRoom.send(message, this, to);
  }

  receive(message, from) {
    console.log(`${this.name} received from ${from.name}: ${message}`);
  }
}

// Usage
const chatRoom = new ChatRoom();
const john = new User('John');
const jane = new User('Jane');
const bob = new User('Bob');

chatRoom.register(john);
chatRoom.register(jane);
chatRoom.register(bob);

john.send('Hello everyone!'); // Broadcast
jane.send('Hi John!', john); // Private message

Memento Pattern

Captures and externalizes an object's internal state so it can be restored later without violating encapsulation.

When to Use:

  • Undo/Redo functionality
  • Snapshot/Restore operations
  • Transaction rollback
  • Save/Load game states
JavaScript Example
// Memento
class EditorMemento {
  constructor(content) {
    this.content = content;
  }

  getContent() {
    return this.content;
  }
}

// Originator
class Editor {
  constructor() {
    this.content = '';
  }

  type(words) {
    this.content += words;
  }

  getContent() {
    return this.content;
  }

  save() {
    return new EditorMemento(this.content);
  }

  restore(memento) {
    this.content = memento.getContent();
  }
}

// Caretaker
class History {
  constructor() {
    this.mementos = [];
  }

  push(memento) {
    this.mementos.push(memento);
  }

  pop() {
    return this.mementos.pop();
  }
}

// Usage
const editor = new Editor();
const history = new History();

editor.type('First line. ');
history.push(editor.save());

editor.type('Second line. ');
history.push(editor.save());

editor.type('Third line.');
console.log(editor.getContent()); // "First line. Second line. Third line."

editor.restore(history.pop());
console.log(editor.getContent()); // "First line. Second line. "

State Pattern

Allows an object to alter its behavior when its internal state changes, appearing to change its class.

When to Use:

  • Object behavior depends on state
  • Large conditional statements
  • State machines
  • Workflow systems
JavaScript Example
// State interface
class State {
  handle(context) { throw new Error('Must implement handle'); }
}

// Concrete States
class DraftState extends State {
  handle(context) {
    console.log('Document is in draft. Publishing...');
    context.setState(new PublishedState());
  }
}

class PublishedState extends State {
  handle(context) {
    console.log('Document is published. Archiving...');
    context.setState(new ArchivedState());
  }
}

class ArchivedState extends State {
  handle(context) {
    console.log('Document is archived. Cannot modify.');
  }
}

// Context
class Document {
  constructor() {
    this.state = new DraftState();
  }

  setState(state) {
    this.state = state;
  }

  publish() {
    this.state.handle(this);
  }
}

// Usage
const doc = new Document();
doc.publish(); // "Document is in draft. Publishing..."
doc.publish(); // "Document is published. Archiving..."
doc.publish(); // "Document is archived. Cannot modify."

Template Method Pattern

Defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithm's structure.

When to Use:

  • Common algorithm structure with varying steps
  • Framework development
  • Avoiding code duplication
  • Hook methods for customization
JavaScript Example
// Abstract Class
class DataProcessor {
  process() {
    this.readData();
    this.processData();
    this.saveData();
  }

  readData() { throw new Error('Must implement readData'); }
  processData() { throw new Error('Must implement processData'); }
  saveData() { throw new Error('Must implement saveData'); }
}

// Concrete Classes
class CSVProcessor extends DataProcessor {
  readData() {
    console.log('Reading CSV file');
  }

  processData() {
    console.log('Processing CSV data');
  }

  saveData() {
    console.log('Saving CSV results');
  }
}

class JSONProcessor extends DataProcessor {
  readData() {
    console.log('Reading JSON file');
  }

  processData() {
    console.log('Processing JSON data');
  }

  saveData() {
    console.log('Saving JSON results');
  }
}

// Usage
const csvProcessor = new CSVProcessor();
csvProcessor.process();
// Output:
// Reading CSV file
// Processing CSV data
// Saving CSV results

const jsonProcessor = new JSONProcessor();
jsonProcessor.process();
// Output:
// Reading JSON file
// Processing JSON data
// Saving JSON results

Chain of Responsibility Pattern

Passes requests along a chain of handlers, where each handler decides either to process the request or pass it to the next handler.

When to Use:

  • Multiple objects can handle a request
  • Request handlers determined at runtime
  • Middleware pipelines
  • Event bubbling
JavaScript Example
// Handler
class SupportHandler {
  constructor() {
    this.nextHandler = null;
  }

  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }

  handle(request) {
    if (this.nextHandler) {
      return this.nextHandler.handle(request);
    }
    return null;
  }
}

// Concrete Handlers
class TechnicalSupport extends SupportHandler {
  handle(request) {
    if (request.type === 'technical') {
      return `Technical Support: Handling ${request.issue}`;
    }
    return super.handle(request);
  }
}

class BillingSupport extends SupportHandler {
  handle(request) {
    if (request.type === 'billing') {
      return `Billing Support: Handling ${request.issue}`;
    }
    return super.handle(request);
  }
}

class GeneralSupport extends SupportHandler {
  handle(request) {
    return `General Support: Handling ${request.issue}`;
  }
}

// Usage
const technical = new TechnicalSupport();
const billing = new BillingSupport();
const general = new GeneralSupport();

technical.setNext(billing).setNext(general);

console.log(technical.handle({ type: 'billing', issue: 'Invoice' }));
// "Billing Support: Handling Invoice"

console.log(technical.handle({ type: 'other', issue: 'Question' }));
// "General Support: Handling Question"

Visitor Pattern

Separates algorithms from the objects on which they operate, allowing new operations to be added without modifying the objects.

When to Use:

  • Operations on complex object structures
  • Adding operations without modifying classes
  • Compiler design (AST traversal)
  • Document export to multiple formats
JavaScript Example
// Visitor
class ShapeVisitor {
  visitCircle(circle) { throw new Error('Must implement'); }
  visitSquare(square) { throw new Error('Must implement'); }
}

class AreaCalculator extends ShapeVisitor {
  visitCircle(circle) {
    return Math.PI * circle.radius ** 2;
  }

  visitSquare(square) {
    return square.side ** 2;
  }
}

class PerimeterCalculator extends ShapeVisitor {
  visitCircle(circle) {
    return 2 * Math.PI * circle.radius;
  }

  visitSquare(square) {
    return 4 * square.side;
  }
}

// Elements
class Circle {
  constructor(radius) {
    this.radius = radius;
  }

  accept(visitor) {
    return visitor.visitCircle(this);
  }
}

class Square {
  constructor(side) {
    this.side = side;
  }

  accept(visitor) {
    return visitor.visitSquare(this);
  }
}

// Usage
const shapes = [new Circle(5), new Square(4)];
const areaCalc = new AreaCalculator();
const perimCalc = new PerimeterCalculator();

shapes.forEach(shape => {
  console.log(`Area: ${shape.accept(areaCalc).toFixed(2)}`);
  console.log(`Perimeter: ${shape.accept(perimCalc).toFixed(2)}`);
});

Interpreter Pattern

Defines a grammatical representation for a language and provides an interpreter to process sentences in the language.

When to Use:

  • Simple grammar/language
  • Expression evaluation
  • SQL parsing
  • Configuration file processing
JavaScript Example
// Abstract Expression
class Expression {
  interpret(context) { throw new Error('Must implement'); }
}

// Terminal Expressions
class NumberExpression extends Expression {
  constructor(number) {
    super();
    this.number = number;
  }

  interpret(context) {
    return this.number;
  }
}

// Non-terminal Expressions
class AddExpression extends Expression {
  constructor(left, right) {
    super();
    this.left = left;
    this.right = right;
  }

  interpret(context) {
    return this.left.interpret(context) + this.right.interpret(context);
  }
}

class SubtractExpression extends Expression {
  constructor(left, right) {
    super();
    this.left = left;
    this.right = right;
  }

  interpret(context) {
    return this.left.interpret(context) - this.right.interpret(context);
  }
}

// Usage - Representing "5 + 3 - 2"
const expression = new SubtractExpression(
  new AddExpression(
    new NumberExpression(5),
    new NumberExpression(3)
  ),
  new NumberExpression(2)
);

console.log(expression.interpret()); // 6

Best Practices

Don't Overuse Patterns

Design patterns are tools, not goals. Use them when they solve a specific problem, not just because you can.

Understand the Problem First

Make sure you fully understand the problem before applying a pattern. Patterns should emerge from the design, not be forced into it.

Keep It Simple

Start with the simplest solution and refactor to patterns when complexity justifies it. YAGNI (You Aren't Gonna Need It) applies.

Team Communication

Patterns provide a common vocabulary. Use pattern names in discussions to communicate design decisions efficiently.

Conclusion

Design patterns are powerful tools in a developer's toolkit. They represent proven solutions to common problems and facilitate better communication among team members. However, they should be applied judiciously—understanding when NOT to use a pattern is as important as knowing when to use one.

As you gain experience, you'll develop an intuition for which patterns apply to which situations. Keep learning, experimenting, and refining your understanding of these fundamental concepts.

Need Help with Software Architecture?

With 20+ years of experience in software design and architecture, I can help you choose the right patterns and build scalable solutions.

Get in Touch