Skip to main content
โšก Calmops

Behavioral Design Patterns in JavaScript

Behavioral Design Patterns in JavaScript

Behavioral patterns focus on object collaboration and responsibility distribution. This article covers Observer, Strategy, Command, State, Template Method, Iterator, and Chain of Responsibility patterns.

Introduction

Behavioral patterns provide:

  • Object communication
  • Responsibility distribution
  • Flexible behavior
  • Event handling
  • Algorithm encapsulation

Understanding these patterns helps you:

  • Manage object interactions
  • Implement flexible algorithms
  • Handle events effectively
  • Distribute responsibilities
  • Improve code organization

Observer Pattern

Basic Observer

// โœ… Good: Observer pattern
class Subject {
  constructor() {
    this.observers = [];
  }

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

  detach(observer) {
    this.observers = this.observers.filter(o => o !== observer);
  }

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

class Observer {
  update(data) {
    throw new Error('Must implement update');
  }
}

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

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

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

subject.attach(observer1);
subject.attach(observer2);

subject.notify('Hello!');
// Observer 1 received: Hello!
// Observer 2 received: Hello!

Event Emitter

// โœ… Good: Event emitter pattern
class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
  }

  off(event, listener) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(l => l !== listener);
    }
  }

  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(listener => listener(data));
    }
  }

  once(event, listener) {
    const onceWrapper = (data) => {
      listener(data);
      this.off(event, onceWrapper);
    };
    this.on(event, onceWrapper);
  }
}

// Usage
const emitter = new EventEmitter();

emitter.on('user:login', (user) => {
  console.log(`${user} logged in`);
});

emitter.on('user:login', (user) => {
  console.log(`Welcome ${user}!`);
});

emitter.emit('user:login', 'John');
// John logged in
// Welcome John!

Strategy Pattern

Basic Strategy

// โœ… Good: Strategy pattern
class PaymentStrategy {
  pay(amount) {
    throw new Error('Must implement pay');
  }
}

class CreditCardPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Paying $${amount} with credit card`);
    return true;
  }
}

class PayPalPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Paying $${amount} with PayPal`);
    return true;
  }
}

class BitcoinPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Paying ${amount} BTC with Bitcoin`);
    return true;
  }
}

class ShoppingCart {
  constructor(paymentStrategy) {
    this.paymentStrategy = paymentStrategy;
    this.total = 0;
  }

  addItem(price) {
    this.total += price;
  }

  checkout() {
    return this.paymentStrategy.pay(this.total);
  }
}

// Usage
const cart = new ShoppingCart(new CreditCardPayment());
cart.addItem(50);
cart.addItem(30);
cart.checkout(); // Paying $80 with credit card

const cart2 = new ShoppingCart(new PayPalPayment());
cart2.addItem(100);
cart2.checkout(); // Paying $100 with PayPal

Sorting Strategy

// โœ… Good: Sorting strategy
class Sorter {
  constructor(strategy) {
    this.strategy = strategy;
  }

  sort(array) {
    return this.strategy.sort(array);
  }
}

class BubbleSort {
  sort(array) {
    console.log('Sorting with bubble sort');
    return [...array].sort((a, b) => a - b);
  }
}

class QuickSort {
  sort(array) {
    console.log('Sorting with quick sort');
    return [...array].sort((a, b) => a - b);
  }
}

// Usage
const data = [5, 2, 8, 1, 9];

const sorter1 = new Sorter(new BubbleSort());
console.log(sorter1.sort(data)); // [1, 2, 5, 8, 9]

const sorter2 = new Sorter(new QuickSort());
console.log(sorter2.sort(data)); // [1, 2, 5, 8, 9]

Command Pattern

// โœ… Good: Command pattern
class Command {
  execute() {
    throw new Error('Must implement execute');
  }

  undo() {
    throw new Error('Must implement undo');
  }
}

class Light {
  constructor() {
    this.isOn = false;
  }

  turnOn() {
    this.isOn = true;
    console.log('Light is on');
  }

  turnOff() {
    this.isOn = false;
    console.log('Light is off');
  }
}

class TurnOnCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }

  execute() {
    this.light.turnOn();
  }

  undo() {
    this.light.turnOff();
  }
}

class TurnOffCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }

  execute() {
    this.light.turnOff();
  }

  undo() {
    this.light.turnOn();
  }
}

class RemoteControl {
  constructor() {
    this.commands = [];
    this.history = [];
  }

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

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

// Usage
const light = new Light();
const remote = new RemoteControl();

remote.execute(new TurnOnCommand(light)); // Light is on
remote.execute(new TurnOffCommand(light)); // Light is off
remote.undo(); // Light is on
remote.undo(); // Light is off

State Pattern

// โœ… Good: State pattern
class State {
  handle(context) {
    throw new Error('Must implement handle');
  }
}

class SolidState extends State {
  handle(context) {
    console.log('Melting solid to liquid');
    context.setState(new LiquidState());
  }
}

class LiquidState extends State {
  handle(context) {
    console.log('Evaporating liquid to gas');
    context.setState(new GasState());
  }
}

class GasState extends State {
  handle(context) {
    console.log('Condensing gas to liquid');
    context.setState(new LiquidState());
  }
}

class Matter {
  constructor() {
    this.state = new SolidState();
  }

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

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

// Usage
const matter = new Matter();
matter.changeState(); // Melting solid to liquid
matter.changeState(); // Evaporating liquid to gas
matter.changeState(); // Condensing gas to liquid

Template Method Pattern

// โœ… Good: Template method pattern
class DataProcessor {
  process(data) {
    const validated = this.validate(data);
    const transformed = this.transform(validated);
    const result = this.save(transformed);
    return result;
  }

  validate(data) {
    throw new Error('Must implement validate');
  }

  transform(data) {
    throw new Error('Must implement transform');
  }

  save(data) {
    throw new Error('Must implement save');
  }
}

class CSVProcessor extends DataProcessor {
  validate(data) {
    console.log('Validating CSV data');
    return data;
  }

  transform(data) {
    console.log('Transforming CSV to objects');
    return data.split('\n').map(line => line.split(','));
  }

  save(data) {
    console.log('Saving to database');
    return data;
  }
}

class JSONProcessor extends DataProcessor {
  validate(data) {
    console.log('Validating JSON data');
    return JSON.parse(data);
  }

  transform(data) {
    console.log('Transforming JSON');
    return data;
  }

  save(data) {
    console.log('Saving to database');
    return data;
  }
}

// Usage
const csvProcessor = new CSVProcessor();
csvProcessor.process('name,age\nJohn,30');

const jsonProcessor = new JSONProcessor();
jsonProcessor.process('{"name":"John","age":30}');

Iterator Pattern

// โœ… Good: Iterator pattern
class Iterator {
  hasNext() {
    throw new Error('Must implement hasNext');
  }

  next() {
    throw new Error('Must implement next');
  }
}

class ArrayIterator extends Iterator {
  constructor(array) {
    super();
    this.array = array;
    this.index = 0;
  }

  hasNext() {
    return this.index < this.array.length;
  }

  next() {
    return this.array[this.index++];
  }
}

class Collection {
  constructor(items) {
    this.items = items;
  }

  createIterator() {
    return new ArrayIterator(this.items);
  }
}

// Usage
const collection = new Collection([1, 2, 3, 4, 5]);
const iterator = collection.createIterator();

while (iterator.hasNext()) {
  console.log(iterator.next());
}
// 1, 2, 3, 4, 5

Chain of Responsibility Pattern

// โœ… Good: Chain of responsibility
class Handler {
  constructor(successor = null) {
    this.successor = successor;
  }

  handle(request) {
    throw new Error('Must implement handle');
  }
}

class AuthHandler extends Handler {
  handle(request) {
    if (!request.user) {
      console.log('Auth failed');
      return false;
    }
    console.log('Auth passed');
    return this.successor ? this.successor.handle(request) : true;
  }
}

class ValidationHandler extends Handler {
  handle(request) {
    if (!request.data) {
      console.log('Validation failed');
      return false;
    }
    console.log('Validation passed');
    return this.successor ? this.successor.handle(request) : true;
  }
}

class LoggingHandler extends Handler {
  handle(request) {
    console.log('Logging request');
    return this.successor ? this.successor.handle(request) : true;
  }
}

// Usage
const chain = new AuthHandler(
  new ValidationHandler(
    new LoggingHandler()
  )
);

const request = { user: 'John', data: { name: 'John' } };
chain.handle(request);
// Auth passed
// Validation passed
// Logging request

Practical Examples

Form Validation

// โœ… Good: Form validation with chain of responsibility
class ValidationRule {
  constructor(successor = null) {
    this.successor = successor;
  }

  validate(data) {
    throw new Error('Must implement validate');
  }
}

class RequiredRule extends ValidationRule {
  validate(data) {
    if (!data.value) {
      return { valid: false, error: 'Field is required' };
    }
    return this.successor ? this.successor.validate(data) : { valid: true };
  }
}

class EmailRule extends ValidationRule {
  validate(data) {
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.value)) {
      return { valid: false, error: 'Invalid email format' };
    }
    return this.successor ? this.successor.validate(data) : { valid: true };
  }
}

class LengthRule extends ValidationRule {
  constructor(minLength, successor = null) {
    super(successor);
    this.minLength = minLength;
  }

  validate(data) {
    if (data.value.length < this.minLength) {
      return { valid: false, error: `Minimum length is ${this.minLength}` };
    }
    return this.successor ? this.successor.validate(data) : { valid: true };
  }
}

// Usage
const emailValidator = new RequiredRule(
  new EmailRule(
    new LengthRule(5)
  )
);

console.log(emailValidator.validate({ value: '' }));
// { valid: false, error: 'Field is required' }

console.log(emailValidator.validate({ value: 'invalid' }));
// { valid: false, error: 'Invalid email format' }

console.log(emailValidator.validate({ value: '[email protected]' }));
// { valid: true }

Notification System

// โœ… Good: Notification system with observer
class NotificationCenter {
  constructor() {
    this.subscribers = {};
  }

  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push(callback);
  }

  unsubscribe(event, callback) {
    if (this.subscribers[event]) {
      this.subscribers[event] = this.subscribers[event].filter(
        cb => cb !== callback
      );
    }
  }

  publish(event, data) {
    if (this.subscribers[event]) {
      this.subscribers[event].forEach(callback => callback(data));
    }
  }
}

// Usage
const notificationCenter = new NotificationCenter();

notificationCenter.subscribe('user:login', (user) => {
  console.log(`${user} logged in`);
});

notificationCenter.subscribe('user:login', (user) => {
  console.log(`Send welcome email to ${user}`);
});

notificationCenter.publish('user:login', 'John');
// John logged in
// Send welcome email to John

Best Practices

  1. Use Observer for event handling:

    // โœ… Good
    emitter.on('event', callback);
    
    // โŒ Bad
    if (condition) callback();
    
  2. Use Strategy for algorithm selection:

    // โœ… Good
    const processor = new Processor(strategy);
    
    // โŒ Bad
    if (type === 'a') { } else if (type === 'b') { }
    
  3. Use State for state management:

    // โœ… Good
    context.setState(newState);
    
    // โŒ Bad
    if (state === 'a') { } else if (state === 'b') { }
    

Common Mistakes

  1. Overusing Observer:

    // โŒ Bad - too many observers
    emitter.on('event', cb1);
    emitter.on('event', cb2);
    emitter.on('event', cb3);
    
    // โœ… Good - group related observers
    emitter.on('event', aggregatedCallback);
    
  2. Command without undo:

    // โŒ Bad
    class Command {
      execute() { }
    }
    
    // โœ… Good
    class Command {
      execute() { }
      undo() { }
    }
    

Summary

Behavioral patterns manage object interactions. Key takeaways:

  • Observer: Event handling
  • Strategy: Algorithm selection
  • Command: Action encapsulation
  • State: State management
  • Template Method: Algorithm structure
  • Iterator: Sequential access
  • Chain of Responsibility: Request handling
  • Improves flexibility
  • Reduces coupling
  • Enhances maintainability

Next Steps

Comments