Patterns: Part two: Behavioral patterns for dynamic applications
Introduction
These patterns focus on how objects interact and respond to changes. They’re great for making apps more dynamic and flexible—perfect for when your code needs to adapt or communicate smoothly.
Target audience: Developers ready to explore more sophisticated patterns for apps with dynamic behaviors and interactions.
In order of practical use
-
JavaScript
Strategy pattern
// Define strategies for different monster behaviors const sleepStrategy = { execute(monster) { console.log(`${monster.name} curls up and falls asleep.`); }, }; const eatStrategy = { execute(monster) { console.log(`${monster.name} munches on their favorite snack.`); }, }; const playStrategy = { execute(monster) { console.log(`${monster.name} runs around, having the time of their life!`); }, }; // Monster class that uses a behavior strategy class Monster { constructor(name, behavior = sleepStrategy) { this.name = name; this.behavior = behavior; } setBehavior(strategy) { this.behavior = strategy; } performAction() { this.behavior.execute(this); } } // Example Usage const chunky = new Monster("Chunky", eatStrategy); chunky.performAction(); // Output: Chunky munches on their favorite snack. const munchy = new Monster("Munchy", playStrategy); munchy.performAction(); // Output: Munchy runs around, having the time of their life! chunky.setBehavior(playStrategy); chunky.performAction(); // Output: Chunky runs around, having the time of their life!
How do you handle a situation where an object needs to perform different behaviors depending on the context? For example, a monster might need to sleep, eat, or play based on what’s happening in your app. You could write a bunch of if/else statements, but that approach gets messy fast.
The Strategy Pattern solves this problem by allowing you to define a family of behaviors (strategies) and dynamically swap them in and out as needed. Instead of hardcoding the behavior into the object, you pass in a strategy that tells the object what to do.
In this example, each monster can have different behaviors like sleepStrategy, eatStrategy, or playStrategy. By default, a monster might sleep, but you can dynamically set their behavior to eating or playing. This pattern makes your code more flexible and easier to maintain.
This pattern is classified as Behavioral because it focuses on defining and managing dynamic behavior.
-
JavaScript
Decorator pattern
// Base Monster class class Monster { constructor(name) { this.name = name; } getDescription() { return `${this.name}`; } } // Decorators function withWings(monster) { monster.fly = () => console.log(`${monster.name} soars into the sky!`); const originalDescription = monster.getDescription; monster.getDescription = function () { return `${originalDescription.call(this)} with wings`; }; return monster; } function withFireBreath(monster) { monster.breatheFire = () => console.log(`${monster.name} breathes a blazing inferno!`); const originalDescription = monster.getDescription; monster.getDescription = function () { return `${originalDescription.call(this)} with fire breath`; }; return monster; } // Example Usage let chunky = new Monster("Chunky"); console.log(chunky.getDescription()); // Output: "Chunky" chunky = withWings(chunky); console.log(chunky.getDescription()); // Output: "Chunky with wings" chunky.fly(); // Output: "Chunky soars into the sky!" chunky = withFireBreath(chunky); console.log(chunky.getDescription()); // Output: "Chunky with wings with fire breath" chunky.breatheFire(); // Output: "Chunky breathes a blazing inferno!"
How do you add new features to an object without modifying its original code? For example, you might start with a plain monster and later decide it can fly or breathe fire. You could subclass the monster or change its code, but that approach can make things inflexible and harder to maintain.
The Decorator Pattern solves this problem by wrapping objects with new functionality. Instead of modifying or subclassing the original object, you use decorators to add features dynamically.
In this example, withWings and withFireBreath are decorators that add abilities to the monster, like flying and breathing fire. Each decorator updates the monster’s behavior and description while keeping the original object intact.
This pattern is classified as Structural because it focuses on composing objects by adding new responsibilities.
-
JavaScript
Chain of responsibility pattern
// Base Handler class class MonsterHandler { setNext(handler) { this.nextHandler = handler; return handler; // Enables chaining } handle(request) { if (this.nextHandler) { return this.nextHandler.handle(request); } console.log("No handler could process the request."); } } // Concrete Handlers class FeedMonsterHandler extends MonsterHandler { handle(request) { if (request.type === "feed") { console.log(`${request.monster.name} is happily fed!`); } else { super.handle(request); } } } class PlayWithMonsterHandler extends MonsterHandler { handle(request) { if (request.type === "play") { console.log(`${request.monster.name} is playing joyfully!`); } else { super.handle(request); } } } class PutMonsterToBedHandler extends MonsterHandler { handle(request) { if (request.type === "sleep") { console.log(`${request.monster.name} is snuggled up and asleep.`); } else { super.handle(request); } } } // Example Usage const chunky = { name: "Chunky" }; // Create handlers const feedHandler = new FeedMonsterHandler(); const playHandler = new PlayWithMonsterHandler(); const sleepHandler = new PutMonsterToBedHandler(); // Chain handlers feedHandler.setNext(playHandler).setNext(sleepHandler); // Send requests through the chain feedHandler.handle({ type: "feed", monster: chunky }); // Output: Chunky is happily fed! feedHandler.handle({ type: "play", monster: chunky }); // Output: Chunky is playing joyfully! feedHandler.handle({ type: "sleep", monster: chunky }); // Output: Chunky is snuggled up and asleep. feedHandler.handle({ type: "unknown", monster: chunky }); // Output: No handler could process the request.
How do you handle a sequence of possible actions without hardcoding every single scenario? For example, if a monster wants to eat, play, or sleep, but you want to process these actions in a flexible and organized way.
The Chain of Responsibility Pattern solves this problem by creating a chain of handlers, each capable of processing a specific type of request. If one handler cannot handle the request, it passes it to the next handler in the chain until someone can handle it (or no one can).
In this example, FeedMonsterHandler, PlayWithMonsterHandler, and PutMonsterToBedHandler are linked together to process requests like feeding, playing, or sleeping. Each handler checks if it can handle the request and either processes it or passes it along the chain.
This pattern is classified as Behavioral because it focuses on managing responsibility and delegation between objects.
-
JavaScript
Command pattern
// Command interface class Command { execute() {} undo() {} } // Concrete commands class FeedMonsterCommand extends Command { constructor(monster) { super(); this.monster = monster; } execute() { console.log(`${this.monster.name} is happily fed.`); this.monster.isFed = true; } undo() { console.log(`Undo: ${this.monster.name} is now hungry.`); this.monster.isFed = false; } } class PlayWithMonsterCommand extends Command { constructor(monster) { super(); this.monster = monster; } execute() { console.log(`${this.monster.name} is playing joyfully.`); this.monster.isPlaying = true; } undo() { console.log(`Undo: ${this.monster.name} has stopped playing.`); this.monster.isPlaying = false; } } // Invoker class MonsterActionInvoker { constructor() { this.history = []; } executeCommand(command) { command.execute(); this.history.push(command); } undoLastCommand() { const command = this.history.pop(); if (command) { command.undo(); } else { console.log("No commands to undo."); } } } // Example Usage const chunky = { name: "Chunky" }; const feedCommand = new FeedMonsterCommand(chunky); const playCommand = new PlayWithMonsterCommand(chunky); const invoker = new MonsterActionInvoker(); // Execute commands invoker.executeCommand(feedCommand); // Output: Chunky is happily fed. invoker.executeCommand(playCommand); // Output: Chunky is playing joyfully. // Undo last command invoker.undoLastCommand(); // Output: Undo: Chunky has stopped playing. invoker.undoLastCommand(); // Output: Undo: Chunky is now hungry.
How do you encapsulate actions (like feeding or playing with a monster) so they can be executed, stored, or undone dynamically? This is especially useful when you need to track user actions or implement features like undo/redo.
The Command Pattern encapsulates requests or actions as objects. This allows you to decouple the object that issues a command (the invoker) from the one that actually executes it. In this example, feeding and playing are represented as commands, and the MonsterActionInvoker tracks and executes them while also allowing you to undo actions.
This pattern is classified as Behavioral because it focuses on how objects interact and handle dynamic behavior.
-
JavaScript
State pattern
// Monster States class HappyState { handle(monster) { console.log(`${monster.name} is happy and bouncing around!`); } } class HungryState { handle(monster) { console.log(`${monster.name} is hungry and looking for snacks.`); } } class SleepyState { handle(monster) { console.log(`${monster.name} is sleepy and curling up for a nap.`); } } // Monster Context class Monster { constructor(name) { this.name = name; this.state = new HappyState(); // Default state } setState(state) { this.state = state; console.log(`${this.name} has changed state.`); } performAction() { this.state.handle(this); } } // Example Usage const chunky = new Monster("Chunky"); chunky.performAction(); // Output: Chunky is happy and bouncing around! chunky.setState(new HungryState()); chunky.performAction(); // Output: Chunky is hungry and looking for snacks. chunky.setState(new SleepyState()); chunky.performAction(); // Output: Chunky is sleepy and curling up for a nap.
How do you handle objects that need to behave differently based on their current state? For example, a monster might act happy, hungry, or sleepy depending on its current mood. Without the right structure, this could lead to messy if/else logic scattered throughout your code.
The State Pattern solves this by encapsulating each state as a separate class with its own behavior. The object (in this case, Monster) delegates actions to its current state, making the code clean, flexible, and easy to extend.
In this example, Chunky starts out happy, but its behavior changes dynamically when it transitions to the hungry or sleepy state. Each state knows how to handle Chunky’s actions based on the context.
This pattern is classified as Behavioral because it manages dynamic changes in behavior.