Behavioral Patterns Handbook
- Behavioral design patterns focus on defining algorithms and distributing responsibilities among objects, emphasizing patterns of communication between them.
- These patterns facilitate the management of complex control flow, allowing a shift from controlling the flow to understanding how objects interact.
- Key patterns include:
- Template Method:
- Intent: Define a generic algorithm structure with customizable steps.
- Why: Allows subclasses to implement specific steps, providing flexibility.
- Interpreter:
- Intent: Interpret sentences in a language using a class hierarchy.
- Why: Useful for language processing, with each class representing a grammar rule.
- Mediator:
- Intent: Centralize communication between objects to reduce direct dependencies.
- Why: Promotes loose coupling by introducing a mediator object for managing interactions.
- Template Method:
-
Intent: To provide a mechanism for handling requests through a chain of candidate objects, where any candidate may fulfill the request based on run-time conditions.
-
Why: Enables even looser coupling, allowing requests to be sent implicitly through a chain of objects. The handling candidate is determined dynamically at runtime.
-
How: Define a chain of handler objects, each with a method to process the request and a reference to the next handler in the chain. Handlers decide whether to process the request or pass it along.
// Handler interface
class OrderHandler
{
setNext(handler)
{ this.nextHandler = handler; }
handleOrder(order)
{
if (this.nextHandler)
{ this.nextHandler.handleOrder(order); }
}
}
// Concrete handlers
class PaymentHandler extends OrderHandler
{
handleOrder(order)
{
if (order.paymentValidated)
{
console.log('Payment processed successfully.');
} else
{
console.log('Payment validation failed. Passing to the next handler.');
super.handleOrder(order);
}
}
}
class StockHandler extends OrderHandler
{
handleOrder(order)
{
if (order.stockAvailable)
{
console.log('Stock is available. Processing order.');
} else
{
console.log('Stock not available. Passing to the next handler.');
super.handleOrder(order);
}
}
}
class ShippingHandler extends OrderHandler
{
handleOrder(order)
{ console.log('Shipping the order.'); }
}
// Example Usage
const paymentHandler = new PaymentHandler();
const stockHandler = new StockHandler();
const shippingHandler = new ShippingHandler();
paymentHandler.setNext(stockHandler);
stockHandler.setNext(shippingHandler);
// Simulating an order
const order =
{
paymentValidated: true,
stockAvailable: true,
};
// Handling the order
paymentHandler.handleOrder(order);
-
Explanation:
PaymentHandler
,StockHandler
, andShippingHandler
are concrete handlers that handle payment validation, stock availability, and shipping, respectively.- Each handler decides whether it can handle the order or should pass it to the next handler in the chain.
- The client sets up the chain by linking the handlers (
paymentHandler
,stockHandler
, andshippingHandler
) using thesetNext
method. - The order is then passed through the chain using
paymentHandler.handleOrder(order)
. - Handlers process the order based on run-time conditions, and if a condition is not met, they pass the order to the next handler in the chain.
-
Benefits:
- Flexibility: Easily adjust the handling chain at runtime by adding or removing handlers.
- Loose Coupling: The sender of a request is decoupled from its receivers, allowing for easy extension without modifying existing code.
- Dynamic Handling: Candidates in the chain dynamically determine whether they can handle a request based on run-time conditions.
-
Intent: To encapsulate requests as objects, enabling parameterization of clients with various command implementations, queuing or logging of command executions, and support for undoable operations.
-
Why: Offers a flexible and decoupled approach for handling requests without requiring knowledge of specific request details. Facilitates queuing, logging, and undoing of command executions.
-
How: Define a Command interface with an
execute
operation. Concrete Command subclasses specify an action-receiver pair, allowing clients to parameterize objects with different commands. An Invoker sets and executes the command, unaware of the specific details of the command's implementation.
// Command interface
class Command
{
execute() {}
undo() {}
redo() {}
}
// Concrete Command for Typing
class TypeCommand extends Command
{
constructor(text, editor)
{
super();
this.previousText = editor.getText();
this.newText = text;
this.editor = editor;
}
execute()
{ this.editor.type(this.newText); }
undo()
{ this.editor.setText(this.previousText); }
redo()
{ this.execute(); }
}
// Concrete Command for Deleting
class DeleteCommand extends Command
{
constructor(editor)
{
super();
this.previousText = editor.getText();
this.editor = editor;
}
execute()
{ this.editor.delete(); }
undo()
{ this.editor.setText(this.previousText); }
redo()
{ this.execute(); }
}
// Receiver
class TextEditor {
constructor()
{ this.text = ''; }
getText()
{ return this.text; }
setText(text)
{
this.text = text;
console.log(`Editor content: ${text}`);
}
type(newText)
{
this.text += newText;
console.log(`Typed: ${newText}`);
}
delete()
{
if (this.text.length > 0)
{
this.text = this.text.slice(0, -1);
console.log('Deleted last character');
}
}
}
// Invoker with Undo/Redo
class UndoRedoManager
{
constructor()
{
this.commandHistory = [];
this.undoIndex = -1;
}
executeCommand(command)
{
command.execute();
this.commandHistory.push(command);
this.undoIndex = this.commandHistory.length - 1;
}
undo()
{
if (this.undoIndex >= 0)
{
const command = this.commandHistory[this.undoIndex];
command.undo();
this.undoIndex -= 1;
} else
{
console.log('Nothing to undo.');
}
}
redo()
{
if (this.undoIndex < this.commandHistory.length - 1)
{
this.undoIndex += 1;
const command = this.commandHistory[this.undoIndex];
command.redo();
} else
{
console.log('Nothing to redo.');
}
}
}
// Usage
const textEditor = new TextEditor();
const undoRedoManager = new UndoRedoManager();
const typeCommand1 = new TypeCommand('Hello, ', textEditor);
const typeCommand2 = new TypeCommand('world!', textEditor);
const deleteCommand = new DeleteCommand(textEditor);
undoRedoManager.executeCommand(typeCommand1);
undoRedoManager.executeCommand(typeCommand2);
undoRedoManager.undo(); // Undo typing "world!"
undoRedoManager.executeCommand(deleteCommand);
undoRedoManager.redo(); // Redo deleting last character
-
Explanation:
TypeCommand
represents typing text, andDeleteCommand
represents deleting text.TextEditor
is the receiver that holds the current text.UndoRedoManager
manages the execution, undo, and redo of commands.
-
Usage:
- Typing "Hello, " and "world!" into the text editor.
- Undoing the last command (deleting "world!").
- Deleting the last character.
- Redoing the undone command (repeating the deletion).
-
Benefits:
-
Flexibility: Commands encapsulate operations, making it easy to add, modify, or extend functionality without changing client code.
-
Decoupling: The Command pattern decouples senders (clients) from receivers (objects that perform actions), reducing dependencies.
-
Undo/Redo: Commands often support undo and redo operations, providing a convenient way to reverse or repeat user actions.
-
Queueing and Logging: Commands can be queued for execution or logged, allowing for task management, history tracking, and auditing.
-
Dynamic Behavior: Enables dynamic behavior as clients can parameterize objects with different commands at runtime.
-
Composite Commands: Commands can be composed into composite commands, providing a higher level of abstraction for complex operations.
-
Testing: Easier unit testing as commands are isolated and can be tested independently.
// Unit Test function testTypeCommand() { const textEditor = new TextEditor(); const typeCommand = new TypeCommand('Hello, ', textEditor); // Execute the command typeCommand.execute(); // Check if the editor's text is updated const result = textEditor.getText() === 'Hello, '; console.assert(result, 'TypeCommand execution failed.'); } // Run the unit test testTypeCommand();
-
Explanation:
testTypeCommand
is a simple unit test function for the TypeCommand.- It creates a TextEditor and a TypeCommand, executes the command, and checks if the text editor's content is updated as expected.
- By isolating the command logic into a separate class, we can easily test individual commands independently of the rest of the system. This promotes a modular and testable design.
-
-
Command History: Keeps a history of executed commands, useful for implementing features like undo/redo or auditing.
-
Separation of Concerns: Encourages separation of concerns by isolating the logic for an operation in a command class.
-
Extensions: Easily extend the system by introducing new commands without modifying existing code.
-
-
Intent: To provide a mechanism for defining a grammar for a language, creating representations for sentences in that language, and interpreting these sentences using the representations.
-
Why: When a problem can be expressed in sentences of a language and occurs frequently, interpreting these sentences provides a solution. Regular expressions are a common example where sentences (patterns) can be interpreted to match strings.
-
How: Define classes to represent grammar rules (non-terminal symbols) and use these classes to create an abstract syntax tree. Each class has an
interpret
method, and instances of these classes are used to interpret sentences based on the language grammar.
// Abstract class representing a grammar rule
class RegularExpression
{
interpret(context)
{ throw new Error('interpret method must be implemented by concrete expressions'); }
}
// Concrete expression classes
class LiteralExpression extends RegularExpression
{
constructor(literal)
{
super();
this.literal = literal;
}
interpret(context)
{ return context.includes(this.literal); }
}
class AlternationExpression extends RegularExpression
{
constructor(expression1, expression2)
{
super();
this.expression1 = expression1;
this.expression2 = expression2;
}
interpret(context)
{ return this.expression1.interpret(context) || this.expression2.interpret(context); }
}
class SequenceExpression extends RegularExpression
{
constructor(expression1, expression2)
{
super();
this.expression1 = expression1;
this.expression2 = expression2;
}
interpret(context)
{ return this.expression1.interpret(context) && this.expression2.interpret(context); }
}
class RepetitionExpression extends RegularExpression
{
constructor(expression)
{
super();
this.expression = expression;
}
interpret(context)
{
// Simulating zero or more repetitions
let currentContext = context;
while (this.expression.interpret(currentContext))
{
currentContext = currentContext.substring(1); // Consume one character
}
return true; // Always true as we are simulating zero or more repetitions
}
}
// Example Usage
const pattern = new SequenceExpression(
new LiteralExpression('raining'),
new AlternationExpression(
new LiteralExpression('dogs'),
new LiteralExpression('cats')
),
new RepetitionExpression()
);
// Test the pattern
const testSentence1 = 'rainingdogs';
const testSentence2 = 'rainingcatsanddogs';
console.log(pattern.interpret(testSentence1)); // true
console.log(pattern.interpret(testSentence2)); // true
-
Explanation:
RegularExpression
is the abstract class representing a grammar rule.- Concrete expression classes (
LiteralExpression
,AlternationExpression
, etc.) implement theinterpret
method to perform interpretation based on the current context. - The example creates a pattern for sentences like "raining dogs" or "raining cats and dogs."
- The
interpret
method simulates the interpretation of sentences based on the defined grammar.
-
Benefits:
- Flexibility: Easily extend the language by adding new grammar rule classes.
- Reusability: Reuse existing grammar rule classes to create new language constructs.
- Separation of Concerns: Separates the grammar from the interpretation logic, promoting a modular design.
- Ease of Maintenance: Changes to language constructs can be localized to their respective classes, making maintenance easier.
-
Intent: To provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
-
Why: An aggregate object, such as a list, should allow access to its elements without revealing its internal structure. Additionally, the need for different traversal methods without bloating the aggregate interface calls for a solution. The Iterator pattern separates access and traversal responsibility, letting iterator objects handle these concerns.
-
How: Define an Iterator interface with methods like
first
,next
,isDone
, andcurrentItem
. Implement a concrete iterator (ListIterator
) for each aggregate class (List
,SkipList
). Clients use the iterator to traverse elements without knowing the internal structure of the collection.
// Iterator interface
class Iterator
{
first();
next();
isDone();
currentItem();
}
// ConcreteIterator for List
class ListIterator extends Iterator
{ // Implementation of iterator methods }
// Abstract Aggregate class
class AbstractList
{ createIterator(); }
// Concrete Aggregate classes
class List extends AbstractList
{
createIterator()
{ return new ListIterator(this); }
}
class SkipList extends AbstractList
{
createIterator()
{ return new SkipListIterator(this); }
}
-
Explanation:
Iterator
defines an interface for accessing and traversing elements.ListIterator
is a concrete iterator implementation for arrays.AbstractList
is the aggregate object (collection) that contains elements.createIterator
is a factory method in the aggregate to create an iterator.- Clients use the iterator (e.g.,
ListIterator
) to traverse the elements without knowing the internal structure of the collection.
-
Benefits:
- Decoupling: Separates the client code from the details of the collection, promoting a more modular design.
- Flexibility: Enables multiple traversal methods for the same collection without modifying the collection itself.
- Reusability: Iterator classes can be reused across different collections that implement the same interface.
- Polymorphic Iteration: Supports polymorphic iteration by allowing different types of iterators for different collections.
-
Intent: To define an object that encapsulates how a set of objects interact. It promotes loose coupling by keeping objects from referring to each other explicitly and allows you to vary their interactions independently.
-
Why: In a system with many objects that communicate with each other, direct communication can lead to high coupling, making it difficult to maintain and extend. The Mediator pattern centralizes communication logic, reducing dependencies between objects.
-
How: Define a Mediator interface that declares communication methods. Concrete Mediator implements these methods and keeps references to the Colleague objects. Colleague classes refer to the Mediator interface rather than directly to each other.
// Mediator interface class Mediator { send(message, colleague); } // Concrete Mediator class ConcreteMediator extends Mediator { constructor() { this.colleague1 = null; this.colleague2 = null; } setColleague1(colleague) { this.colleague1 = colleague; } setColleague2(colleague) { this.colleague2 = colleague; } send(message, colleague) { if (colleague === this.colleague1) { this.colleague2.receive(message); } else { this.colleague1.receive(message); } } } // Colleague class class Colleague { constructor(mediator) { this.mediator = mediator; } send(message) { this.mediator.send(message, this); } receive(message) { console.log(`Colleague received: ${message}`); } } // Example usage const mediator = new ConcreteMediator(); const colleague1 = new Colleague(mediator); const colleague2 = new Colleague(mediator); mediator.setColleague1(colleague1); mediator.setColleague2(colleague2); colleague1.send('Hello from colleague1!'); // Output: Colleague received: Hello from colleague1! colleague2.send('Hi from colleague2!'); // Output: Colleague received: Hi from colleague2!
-
Explanation:
Mediator
declares the communication interface, andConcreteMediator
implements it, keeping references to Colleague objects.Colleague
objects communicate through the Mediator, reducing direct dependencies between them.- Changes in communication logic are encapsulated within the Mediator, promoting flexibility and maintainability.
-
Example usage:
// Mediator: Chatroom class Chatroom { constructor() { this.users = {}; } registerUser(user) { this.users[user.name] = user; user.chatroom = this; } sendMessage(sender, receiverName, message) { const receiver = this.users[receiverName]; if (receiver) { receiver.receiveMessage(sender, message); } else { console.log(`User '${receiverName}' not found.`); } } } // Colleague: User class User { constructor(name) { this.name = name; this.chatroom = null; } send(message, receiverName) { if (this.chatroom) { console.log(`${this.name} sends message to ${receiverName}: ${message}`); this.chatroom.sendMessage(this, receiverName, message); } else { console.log(`${this.name} is not part of any chatroom.`); } } receiveMessage(sender, message) { console.log(`${this.name} receives message from ${sender.name}: ${message}`); } }
/*---- Test the Chat Application ----*/ // Create a chatroom const chatroom = new Chatroom(); // Create users const alice = new User('Alice'); const bob = new User('Bob'); const charlie = new User('Charlie'); // Register users with the chatroom chatroom.registerUser(alice); chatroom.registerUser(bob); chatroom.registerUser(charlie); // Users send messages alice.send('Hi, Bob!', 'Bob'); bob.send('Hello, everyone!', 'Alice'); charlie.send('Greetings!', 'Bob'); // Expected Output: // Alice sends message to Bob: Hi, Bob! // Bob receives message from Alice: Hi, Bob! // Bob sends message to Alice: Hello, everyone! // Alice receives message from Bob: Hello, everyone! // Charlie sends message to Bob: Greetings! // Bob receives message from Charlie: Greetings!
-
Explanation:
- The
Chatroom
class acts as the mediator. It keeps track of registered users and facilitates message distribution. - The
User
class represents a user in the chat application. Users can send messages to each other using thesend
method. - When a user sends a message, it goes through the
Chatroom
mediator. The mediator ensures that the message is delivered to the appropriate user. - In this example,
user1
sends a message toBob
,user2
sends a message toAlice
, anduser3
sends a message toBob
. The messages are appropriately distributed by the chatroom mediator.
- The
-
-
Benefits:
- Decoupling: Objects are decoupled from each other, as they communicate through the Mediator.
- Centralized Control: Communication logic is centralized, making it easier to manage and modify.
- Reusability: Mediators can be reused with different sets of colleagues.
- Easy Maintenance: Changes in communication logic are localized to the Mediator, reducing the impact on other objects.
- Intent: To capture an object's internal state such that it can be restored to this state later.
- Why: Enables an object to revert to a previous state, supporting undo mechanisms, history tracking, and recovery.
- How: Define three main roles: Originator (the object whose state needs to be saved), Memento (an object storing the state of the Originator), and Caretaker (responsible for keeping track of and restoring the Originator's state). The Memento captures the internal state of the Originator without exposing it.
// Originator: Object whose state needs to be saved
class Editor
{
constructor()
{ this.content = ''; }
type(text)
{ this.content += text; }
save()
{ return new EditorMemento(this.content); }
restore(memento)
{ this.content = memento.getContent(); }
getContent()
{ return this.content; }
}
// Memento: Object storing the state of the Originator
class EditorMemento
{
constructor(content)
{ this.content = content; }
getContent()
{ return this.content; }
}
// Caretaker: Responsible for keeping track of and restoring the Originator's state
class History
{
constructor() {
this.states = [];
}
push(editorMemento)
{ this.states.push(editorMemento); }
pop()
{ return this.states.pop(); }
}
// Example Usage
const editor = new Editor();
const history = new History();
editor.type('This is the first sentence. ');
history.push(editor.save());
editor.type('This is the second sentence. ');
history.push(editor.save());
editor.type('This is the third sentence. ');
// Restore to the second state
const secondState = history.pop();
editor.restore(secondState);
console.log(editor.getContent()); // Output: This is the first sentence. This is the second sentence.
-
Explanation:
Editor
: The object whose state needs to be saved.EditorMemento
: Object storing the state of theEditor
.History
: Keeps track ofEditor
states and allows reverting.
-
Benefits:
- Undo Mechanism: Enables undoing operations by reverting to previous states.
- History Tracking: Facilitates keeping a history of an object's state changes.
- Isolation of Concerns: Separates the responsibility of state management from the actual object.
-
Use Cases:
- Text editors with undo/redo functionality.
- Version control systems.
- Form data persistence and recovery in web applications.
-
Intent: To define a one-to-many dependency between objects, where if one object (the subject) changes state, all its dependents (observers) are notified and updated automatically.
-
Why: Enables a decoupled communication mechanism between objects, allowing changes in one object to propagate to multiple other objects without them being tightly coupled. It ensures a consistent state across dependent objects.
-
How:
- Define a
Subject
interface with methods to add, remove, and notify observers. - Create a
ConcreteSubject
class implementing theSubject
interface. This class maintains state and notifies observers when the state changes. - Define an
Observer
interface with anupdate
method. - Create
ConcreteObserver
classes implementing theObserver
interface. These are the objects interested in the state changes of the subject. - The subject maintains a collection of observers and notifies them whenever its state changes.
- Observers register with the subject to receive updates.
- When the subject's state changes, it iterates through its observers and calls their
update
methods.
- Define a
-
Example Implementation:
// Observer Pattern: Distributed Event Handling // EventBroker: Centralized component managing events class EventBroker { constructor() { this.subscriptions = {}; } subscribe(eventType, subscriber) { if (!this.subscriptions[eventType]) { this.subscriptions[eventType] = []; } this.subscriptions[eventType].push(subscriber); } unsubscribe(eventType, subscriber) { if (this.subscriptions[eventType]) { this.subscriptions[eventType] = this.subscriptions[eventType].filter(sub => sub !== subscriber); } } publish(eventType, data) { if (this.subscriptions[eventType]) { this.subscriptions[eventType].forEach(subscriber => subscriber.notify(eventType, data)); } } } // Service: Represents a service that can publish or subscribe to events class Service { constructor(name, eventBroker) { this.name = name; this.eventBroker = eventBroker; } publishEvent(eventType, data) { console.log(`${this.name} publishing event: ${eventType}`); this.eventBroker.publish(eventType, data); } subscribeToEvent(eventType, callback) { console.log(`${this.name} subscribing to event: ${eventType}`); const subscriber = new EventSubscriber(this, callback); this.eventBroker.subscribe(eventType, subscriber); return subscriber; } unsubscribeFromEvent(eventSubscriber) { console.log(`${this.name} unsubscribing from event: ${eventSubscriber.eventType}`); this.eventBroker.unsubscribe(eventSubscriber.eventType, eventSubscriber); } } // EventSubscriber: Represents an object that subscribes to events class EventSubscriber { constructor(service, callback) { this.service = service; this.callback = callback; this.eventType = null; } notify(eventType, data) { console.log(`${this.service.name} received event: ${eventType}`); this.callback(data); } unsubscribe() { this.service.unsubscribeFromEvent(this); } } // Example Usage const eventBroker = new EventBroker(); const serviceA = new Service('ServiceA', eventBroker); const serviceB = new Service('ServiceB', eventBroker); const subscriberA = serviceA.subscribeToEvent('userLoggedIn', data => { console.log(`SubscriberA handling userLoggedIn event: ${JSON.stringify(data)}`); }); const subscriberB = serviceB.subscribeToEvent('userLoggedIn', data => { console.log(`SubscriberB handling userLoggedIn event: ${JSON.stringify(data)}`); }); serviceA.publishEvent('userLoggedIn', { userId: 123, username: 'userA' }); // Output: // ServiceA publishing event: userLoggedIn // ServiceB subscribing to event: userLoggedIn // SubscriberA handling userLoggedIn event: {"userId":123,"username":"userA"} // SubscriberB handling userLoggedIn event: {"userId":123,"username":"userA"}
-
Explanation:
-
EventBroker
is a centralized component managing event subscriptions and notifications. -
Service
represents a service that can publish or subscribe to events. -
EventSubscriber
is an object that subscribes to events and handles notifications.
ServiceA
publishes a "userLoggedIn" event, and bothServiceA
andServiceB
subscribe to that event. The subscribers (SubscriberA and SubscriberB) handle the event by logging the data.
-
-
Benefits:
-
Loose coupling between subjects and observers.
-
Supports broadcast communication.
-
Enables dynamically adding or removing observers.
-
Promotes a clear separation between concerns, enhancing maintainability and scalability.
-
Use Cases:
- User interface components that need to react to changes in underlying data models.
- Event handling systems in distributed architectures.
- Implementing listeners in graphical user interfaces.
- Maintaining consistency in state across different components of a system.
-
Related Patterns:
- Mediator Pattern: Defines an object that centralizes communication between objects, reducing direct connections.
- Publish-Subscribe Pattern: Similar to the Observer Pattern, but with a third component (message broker) managing subscriptions.
-
-
Intent: To allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
-
Why: The State Pattern is used to represent the various states of an object as separate classes. Instead of using a monolithic and conditional approach to manage an object's behavior, the State Pattern encapsulates each state in a separate class, making the object's behavior more modular, extensible, and maintainable.
-
How:
- Define a
Context
class that maintains a reference to the current state object. - Create a
State
interface or an abstract class that declares methods representing the various states. - Implement concrete state classes that extend the
State
interface. Each concrete state class implements the behavior associated with a specific state. - The
Context
class delegates state-specific behavior to the current state object. - The
Context
class can change its current state, effectively changing its behavior.
- Define a
-
Example:
// State Interface class WorkflowState { handle(context) {} } // Concrete State Classes class DraftState extends WorkflowState { handle(context) { console.log('Document is in Draft state. Ready for review.'); context.setState(new ReviewState()); } } class ReviewState extends WorkflowState { handle(context) { console.log('Document is in Review state. Awaiting approval.'); // Logic for review and transitions // For simplicity, directly transitioning to the ApprovedState context.setState(new ApprovedState()); } } class ApprovedState extends WorkflowState { handle(context) { console.log('Document is Approved. Ready for finalization.'); // Logic for finalization } } class RejectedState extends WorkflowState { handle(context) { console.log('Document is Rejected. Requires revision.'); // Logic for handling rejection and transitions // For simplicity, transitioning back to DraftState context.setState(new DraftState()); } } // Context Class class Document { constructor() { this.state = new DraftState(); // Initial state is Draft } setState(state) { this.state = state; } process() { this.state.handle(this); } } // Example Usage const document = new Document(); document.process(); // Output: Document is in Draft state. Ready for review. document.process(); // Output: Document is in Review state. Awaiting approval. document.process(); // Output: Document is Approved. Ready for finalization.
-
Explanation:
- State Interface: Define a
WorkflowState
interface with ahandle
method. - Concrete States: Implement concrete state classes (e.g.,
Draft
,Review
) that follow theWorkflowState
interface. - Context (Document):
- Create a
Document
class with a current state reference. - The
Document
delegates state-specific behavior to the current state'shandle
method.
- Create a
- Usage:
- Instantiate a
Document
. - Call
handle
to trigger state-specific actions. - Easily add or modify states without changing the
Document
class.
- Instantiate a
- State Interface: Define a
-
Benefits:
- Modularity: Encapsulates the behavior associated with each state in a separate class, promoting modular design.
- Extensibility: Easy to add new states by creating new state classes without modifying existing code.
- Simplifies Code: Eliminates large conditional statements for managing state transitions, making code more readable and maintainable.
- Promotes Clean Design: Clarifies the object's behavior by explicitly defining each state.
- Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
-
Intent: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
-
Why: Offers a framework for algorithms while allowing subclasses to implement specific steps.
-
How: Define a template method in an abstract class, breaking the algorithm into steps. Concrete subclasses implement these steps, customizing the behavior.
-
Example usage: A simple example of a template method pattern for sorting algorithms. In this case, we have a generic SortAlgorithm class with a template method, and then two subclasses (BubbleSort and QuickSort) that provide specific implementations for comparison and swapping steps.
class SortAlgorithm
{
// Template method defining the generic sorting algorithm structure
sort(array)
{
const n = array.length;
for (let i = 0; i < n - 1; i++)
{
for (let j = 0; j < n - i - 1; j++)
{
// Algorithm-specific comparison and swapping steps
if (this.shouldSwap(array[j], array[j + 1]))
{ this.swap(array, j, j + 1); }
}
}
}
// Abstract methods to be implemented by subclasses
shouldSwap(element1, element2)
{ throw new Error('shouldSwap must be implemented by subclasses'); }
swap(array, index1, index2)
{ throw new Error('swap must be implemented by subclasses'); }
}
// Concrete Sorting Algorithm: BubbleSort
class BubbleSort extends SortAlgorithm
{
shouldSwap(element1, element2)
{
// Algorithm-specific comparison logic for BubbleSort
return element1 > element2;
}
swap(array, index1, index2)
{
// Algorithm-specific swapping logic for BubbleSort
const temp = array[index1];
array[index1] = array[index2];
array[index2] = temp;
}
}
// Concrete Sorting Algorithm: QuickSort
class QuickSort extends SortAlgorithm
{
shouldSwap(element1, element2)
{
// Algorithm-specific comparison logic for QuickSort
return element1 > element2;
}
swap(array, index1, index2)
{
// Algorithm-specific swapping logic for QuickSort
const temp = array[index1];
array[index1] = array[index2];
array[index2] = temp;
}
}
- Explanation:
SortAlgorithm
is the abstract class defining the template method (sort) and declaring abstract methods (shouldSwap and swap) to be implemented by concrete subclasses.BubbleSort
andQuickSort
are concrete subclasses that inherit fromSortAlgorithm
and provide specific implementations for the abstract methods based on their sorting strategies.- The sort method in
SortAlgorithm
uses the template method pattern, calling the abstract methods for algorithm-specific comparison and swapping steps.
// Example Usage
const arrayToSort = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
// BubbleSort
const bubbleSort = new BubbleSort();
const arrayBubbleSort = arrayToSort.slice();
bubbleSort.sort(arrayBubbleSort);
console.log('BubbleSort Result:', arrayBubbleSort);
// QuickSort
const quickSort = new QuickSort();
const arrayQuickSort = arrayToSort.slice();
quickSort.sort(arrayQuickSort);
console.log('QuickSort Result:', arrayQuickSort);
/*-- Logs --*/
BubbleSort Result: [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
QuickSort Result: [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
- Intent: Represent an operation to be performed on the elements of an objectstructure. Visitor lets you define a new operation without changing theclasses of the elements on which it operates.
Subscribe to the Newsletter
Get my latest posts and project updates by email