Behavioral Patterns
Learning Objectives
- By the end of this lesson, you will be able to:
- - Understand behavioral design patterns
- - Implement the Observer pattern
- - Implement the Strategy pattern
- - Implement the Command pattern
- - Choose appropriate patterns
- - Apply patterns to solve problems
- - Build flexible communication systems
Lesson 19.3: Behavioral Patterns
Learning Objectives
By the end of this lesson, you will be able to:
- Understand behavioral design patterns
- Implement the Observer pattern
- Implement the Strategy pattern
- Implement the Command pattern
- Choose appropriate patterns
- Apply patterns to solve problems
- Build flexible communication systems
Introduction to Behavioral Patterns
Behavioral patterns focus on communication between objects and how responsibilities are distributed.
Why Behavioral Patterns?
- Communication: Better object communication
- Flexibility: Easier to change behavior
- Reusability: Reuse communication patterns
- Maintainability: Clearer responsibilities
- Decoupling: Reduce dependencies
- Best Practices: Proven communication solutions
Common Behavioral Patterns
- Observer: Notify multiple objects of changes
- Strategy: Encapsulate algorithms
- Command: Encapsulate requests
Observer Pattern
What is Observer?
Observer defines a one-to-many dependency between objects so when one changes, all dependents are notified.
Basic Observer
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
let subject = new Subject();
let observer1 = new Observer('Observer 1');
let observer2 = new Observer('Observer 2');
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('Hello');
// Observer 1 received: Hello
// Observer 2 received: Hello
Observer with Events
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
}
// Usage
let emitter = new EventEmitter();
emitter.on('userLogin', (user) => {
console.log('User logged in:', user);
});
emitter.on('userLogin', (user) => {
console.log('Sending welcome email to:', user.email);
});
emitter.emit('userLogin', { name: 'Alice', email: 'alice@example.com' });
Practical Observer Example
class Stock {
constructor(symbol, price) {
this.symbol = symbol;
this.price = price;
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
setPrice(price) {
if (this.price !== price) {
this.price = price;
this.notify();
}
}
notify() {
this.observers.forEach(observer => {
observer.update(this.symbol, this.price);
});
}
}
class PriceDisplay {
constructor() {
this.prices = {};
}
update(symbol, price) {
this.prices[symbol] = price;
console.log(`${symbol}: $${price}`);
}
getPrice(symbol) {
return this.prices[symbol];
}
}
// Usage
let stock = new Stock('AAPL', 150);
let display = new PriceDisplay();
stock.subscribe(display);
stock.setPrice(155); // AAPL: $155
stock.setPrice(160); // AAPL: $160
Strategy Pattern
What is Strategy?
Strategy defines a family of algorithms, encapsulates each, and makes them interchangeable.
Basic Strategy
// Strategy interface
class PaymentStrategy {
pay(amount) {
throw new Error('Method must be implemented');
}
}
// Concrete strategies
class CreditCardStrategy extends PaymentStrategy {
constructor(cardNumber, cvv) {
super();
this.cardNumber = cardNumber;
this.cvv = cvv;
}
pay(amount) {
console.log(`Paid $${amount} using credit card ${this.cardNumber}`);
}
}
class PayPalStrategy extends PaymentStrategy {
constructor(email) {
super();
this.email = email;
}
pay(amount) {
console.log(`Paid $${amount} using PayPal ${this.email}`);
}
}
class BitcoinStrategy extends PaymentStrategy {
constructor(walletAddress) {
super();
this.walletAddress = walletAddress;
}
pay(amount) {
console.log(`Paid $${amount} using Bitcoin ${this.walletAddress}`);
}
}
// Context
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
processPayment(amount) {
this.strategy.pay(amount);
}
}
// Usage
let processor = new PaymentProcessor(new CreditCardStrategy('1234-5678', '123'));
processor.processPayment(100);
processor.setStrategy(new PayPalStrategy('alice@example.com'));
processor.processPayment(200);
Strategy with Functions
// Strategy as functions
let strategies = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
};
class Calculator {
constructor() {
this.strategy = strategies.add;
}
setStrategy(operation) {
if (strategies[operation]) {
this.strategy = strategies[operation];
} else {
throw new Error('Unknown operation');
}
}
calculate(a, b) {
return this.strategy(a, b);
}
}
// Usage
let calc = new Calculator();
console.log(calc.calculate(10, 5)); // 15
calc.setStrategy('multiply');
console.log(calc.calculate(10, 5)); // 50
Practical Strategy Example
// Sorting strategies
class SortStrategy {
sort(array) {
throw new Error('Method must be implemented');
}
}
class BubbleSort extends SortStrategy {
sort(array) {
let arr = [...array];
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
}
class QuickSort extends SortStrategy {
sort(array) {
if (array.length <= 1) return array;
let pivot = array[Math.floor(array.length / 2)];
let left = array.filter(x => x < pivot);
let middle = array.filter(x => x === pivot);
let right = array.filter(x => x > pivot);
return [...this.sort(left), ...middle, ...this.sort(right)];
}
}
class Sorter {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
sort(array) {
return this.strategy.sort(array);
}
}
// Usage
let numbers = [64, 34, 25, 12, 22, 11, 90];
let sorter = new Sorter(new BubbleSort());
console.log('Bubble sort:', sorter.sort(numbers));
sorter.setStrategy(new QuickSort());
console.log('Quick sort:', sorter.sort(numbers));
Command Pattern
What is Command?
Command encapsulates a request as an object, allowing parameterization and queuing of requests.
Basic Command
// Command interface
class Command {
execute() {
throw new Error('Method must be implemented');
}
undo() {
throw new Error('Method must be implemented');
}
}
// Concrete commands
class LightOnCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() {
this.light.turnOn();
}
undo() {
this.light.turnOff();
}
}
class LightOffCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() {
this.light.turnOff();
}
undo() {
this.light.turnOn();
}
}
// Receiver
class Light {
turnOn() {
console.log('Light is ON');
}
turnOff() {
console.log('Light is OFF');
}
}
// Invoker
class RemoteControl {
constructor() {
this.command = null;
this.history = [];
}
setCommand(command) {
this.command = command;
}
pressButton() {
if (this.command) {
this.command.execute();
this.history.push(this.command);
}
}
undo() {
if (this.history.length > 0) {
let lastCommand = this.history.pop();
lastCommand.undo();
}
}
}
// Usage
let light = new Light();
let lightOn = new LightOnCommand(light);
let lightOff = new LightOffCommand(light);
let remote = new RemoteControl();
remote.setCommand(lightOn);
remote.pressButton(); // Light is ON
remote.setCommand(lightOff);
remote.pressButton(); // Light is OFF
remote.undo(); // Light is ON
Command with Queue
class CommandQueue {
constructor() {
this.queue = [];
this.executing = false;
}
add(command) {
this.queue.push(command);
if (!this.executing) {
this.executeNext();
}
}
async executeNext() {
if (this.queue.length === 0) {
this.executing = false;
return;
}
this.executing = true;
let command = this.queue.shift();
await command.execute();
this.executeNext();
}
}
// Usage
class DelayCommand {
constructor(delay, message) {
this.delay = delay;
this.message = message;
}
async execute() {
return new Promise(resolve => {
setTimeout(() => {
console.log(this.message);
resolve();
}, this.delay);
});
}
}
let queue = new CommandQueue();
queue.add(new DelayCommand(1000, 'First'));
queue.add(new DelayCommand(500, 'Second'));
queue.add(new DelayCommand(200, 'Third'));
// Executes in order
Practical Command Example
// Text editor commands
class TextEditor {
constructor() {
this.content = '';
this.history = [];
}
write(text) {
this.content += text;
}
delete(length) {
this.content = this.content.slice(0, -length);
}
getContent() {
return this.content;
}
}
class WriteCommand {
constructor(editor, text) {
this.editor = editor;
this.text = text;
}
execute() {
this.editor.write(this.text);
this.editor.history.push(this);
}
undo() {
this.editor.delete(this.text.length);
}
}
class DeleteCommand {
constructor(editor, length) {
this.editor = editor;
this.length = length;
this.deletedText = '';
}
execute() {
this.deletedText = this.editor.content.slice(-this.length);
this.editor.delete(this.length);
this.editor.history.push(this);
}
undo() {
this.editor.write(this.deletedText);
}
}
// Usage
let editor = new TextEditor();
let writeHello = new WriteCommand(editor, 'Hello');
let writeWorld = new WriteCommand(editor, ' World');
writeHello.execute();
writeWorld.execute();
console.log(editor.getContent()); // 'Hello World'
let lastCommand = editor.history.pop();
lastCommand.undo();
console.log(editor.getContent()); // 'Hello'
Practice Exercise
Exercise: Behavioral Patterns
Objective: Practice implementing Observer, Strategy, and Command patterns.
Instructions:
- Create a JavaScript file
- Practice:
- Creating observers
- Implementing strategies
- Building commands
- Applying patterns to real problems
Example Solution:
// behavioral-patterns-practice.js
console.log("=== Behavioral Patterns Practice ===");
console.log("\n=== Observer Pattern ===");
// Basic Observer
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);
}
}
let subject = new Subject();
let observer1 = new Observer('Observer 1');
let observer2 = new Observer('Observer 2');
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('Hello');
subject.unsubscribe(observer1);
subject.notify('World');
// Event Emitter
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
}
let emitter = new EventEmitter();
emitter.on('userLogin', (user) => {
console.log('User logged in:', user.name);
});
emitter.on('userLogin', (user) => {
console.log('Sending email to:', user.email);
});
emitter.emit('userLogin', { name: 'Alice', email: 'alice@example.com' });
console.log();
console.log("=== Strategy Pattern ===");
// Payment strategies
class PaymentStrategy {
pay(amount) {
throw new Error('Method must be implemented');
}
}
class CreditCardStrategy extends PaymentStrategy {
constructor(cardNumber) {
super();
this.cardNumber = cardNumber;
}
pay(amount) {
console.log(`Paid $${amount} using credit card ${this.cardNumber}`);
}
}
class PayPalStrategy extends PaymentStrategy {
constructor(email) {
super();
this.email = email;
}
pay(amount) {
console.log(`Paid $${amount} using PayPal ${this.email}`);
}
}
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
processPayment(amount) {
this.strategy.pay(amount);
}
}
let processor = new PaymentProcessor(new CreditCardStrategy('1234-5678'));
processor.processPayment(100);
processor.setStrategy(new PayPalStrategy('alice@example.com'));
processor.processPayment(200);
// Function strategies
let strategies = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
};
class Calculator {
constructor() {
this.strategy = strategies.add;
}
setStrategy(operation) {
if (strategies[operation]) {
this.strategy = strategies[operation];
} else {
throw new Error('Unknown operation');
}
}
calculate(a, b) {
return this.strategy(a, b);
}
}
let calc = new Calculator();
console.log('10 + 5 =', calc.calculate(10, 5));
calc.setStrategy('multiply');
console.log('10 * 5 =', calc.calculate(10, 5));
console.log();
console.log("=== Command Pattern ===");
// Basic Command
class Command {
execute() {
throw new Error('Method must be implemented');
}
undo() {
throw new Error('Method must be implemented');
}
}
class LightOnCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() {
this.light.turnOn();
}
undo() {
this.light.turnOff();
}
}
class LightOffCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() {
this.light.turnOff();
}
undo() {
this.light.turnOn();
}
}
class Light {
turnOn() {
console.log('Light is ON');
}
turnOff() {
console.log('Light is OFF');
}
}
class RemoteControl {
constructor() {
this.history = [];
}
executeCommand(command) {
command.execute();
this.history.push(command);
}
undo() {
if (this.history.length > 0) {
let lastCommand = this.history.pop();
lastCommand.undo();
}
}
}
let light = new Light();
let remote = new RemoteControl();
remote.executeCommand(new LightOnCommand(light));
remote.executeCommand(new LightOffCommand(light));
remote.undo();
// Text Editor Commands
class TextEditor {
constructor() {
this.content = '';
this.history = [];
}
write(text) {
this.content += text;
}
delete(length) {
this.content = this.content.slice(0, -length);
}
getContent() {
return this.content;
}
}
class WriteCommand {
constructor(editor, text) {
this.editor = editor;
this.text = text;
}
execute() {
this.editor.write(this.text);
this.editor.history.push(this);
}
undo() {
this.editor.delete(this.text.length);
}
}
let editor = new TextEditor();
let writeHello = new WriteCommand(editor, 'Hello');
let writeWorld = new WriteCommand(editor, ' World');
writeHello.execute();
writeWorld.execute();
console.log('Content:', editor.getContent());
let lastCommand = editor.history.pop();
lastCommand.undo();
console.log('After undo:', editor.getContent());
Expected Output:
=== Behavioral Patterns Practice ===
=== Observer Pattern ===
Observer 1 received: Hello
Observer 2 received: Hello
Observer 2 received: World
User logged in: Alice
Sending email to: alice@example.com
=== Strategy Pattern ===
Paid $100 using credit card 1234-5678
Paid $200 using PayPal alice@example.com
10 + 5 = 15
10 * 5 = 50
=== Command Pattern ===
Light is ON
Light is OFF
Light is ON
Content: Hello World
After undo: Hello
Challenge (Optional):
- Build a complete application using all three patterns
- Create an event system
- Build a plugin system
- Practice combining patterns
Common Mistakes
1. Observer Memory Leaks
// ⚠️ Problem: Observers not removed
subject.subscribe(observer);
// Observer never unsubscribed
// ✅ Solution: Always unsubscribe
subject.subscribe(observer);
// Later...
subject.unsubscribe(observer);
2. Strategy Not Swappable
// ⚠️ Problem: Strategy hardcoded
class Processor {
process() {
// Hardcoded strategy
}
}
// ✅ Solution: Make strategy swappable
class Processor {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
}
3. Command Without Undo
// ⚠️ Problem: No undo capability
class Command {
execute() { }
// Missing undo
}
// ✅ Solution: Implement undo
class Command {
execute() { }
undo() { }
}
Key Takeaways
- Observer: Notify multiple objects of changes
- Strategy: Encapsulate interchangeable algorithms
- Command: Encapsulate requests as objects
- When to Use: Choose based on communication needs
- Best Practice: Keep patterns simple and focused
- Memory: Unsubscribe observers to prevent leaks
- Flexibility: Patterns enable runtime behavior changes
Quiz: Behavioral Patterns
Test your understanding with these questions:
-
Observer pattern:
- A) One-to-many dependency
- B) Many-to-one dependency
- C) One-to-one
- D) None
-
Strategy pattern:
- A) Encapsulates algorithms
- B) Encapsulates data
- C) Encapsulates nothing
- D) Random
-
Command pattern:
- A) Encapsulates requests
- B) Encapsulates responses
- C) Encapsulates nothing
- D) Random
-
Observer subscribe:
- A) Adds observer
- B) Removes observer
- C) Notifies observer
- D) Nothing
-
Strategy is:
- A) Swappable
- B) Fixed
- C) Random
- D) None
-
Command can:
- A) Execute
- B) Undo
- C) Both
- D) Neither
-
Behavioral patterns focus on:
- A) Communication
- B) Creation
- C) Structure
- D) Nothing
Answers:
- A) One-to-many dependency
- A) Encapsulates algorithms
- A) Encapsulates requests
- A) Adds observer
- A) Swappable
- C) Both
- A) Communication
Next Steps
Congratulations! You've completed Module 19: Design Patterns. You now know:
- Creational patterns (Singleton, Factory, Builder)
- Structural patterns (Module, Facade, Decorator)
- Behavioral patterns (Observer, Strategy, Command)
- When to use each pattern
What's Next?
- Module 20: Error Handling and Debugging
- Lesson 20.1: Advanced Error Handling
- Learn error types and handling strategies
- Build robust error handling
Additional Resources
- MDN: Error Handling: developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling
- Design Patterns: refactoring.guru/design-patterns
Lesson completed! You've finished Module 19: Design Patterns. Ready for Module 20: Error Handling and Debugging!
Course Navigation
- Creational Patterns
- Structural Patterns
- Behavioral Patterns