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
-
Use Observer for event handling:
// โ Good emitter.on('event', callback); // โ Bad if (condition) callback(); -
Use Strategy for algorithm selection:
// โ Good const processor = new Processor(strategy); // โ Bad if (type === 'a') { } else if (type === 'b') { } -
Use State for state management:
// โ Good context.setState(newState); // โ Bad if (state === 'a') { } else if (state === 'b') { }
Common Mistakes
-
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); -
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
Related Resources
- Behavioral Patterns - Refactoring Guru
- Observer Pattern - MDN
- Strategy Pattern - Wikipedia
- State Pattern - Wikipedia
- Design Patterns in JavaScript
Next Steps
- Learn about Dependency Injection
- Explore Architectural Patterns
- Study SOLID Principles
- Practice behavioral patterns
- Build event-driven systems
Comments