Behavioral design patterns are a category of design patterns that identify common solutions to software design problems related to communication between objects. These patterns focus on the interaction and responsibilities between objects, rather than their structure or implementation. In this tutorial, we will explore several behavioral patterns through practical exercises that you can apply in real-world scenarios.
Behavioral patterns are classified into three main categories:
Problem Statement: You have a series of request handlers that can process different types of requests. Each handler decides whether to process the request or pass it on to the next handler in the chain.
Exercise:
Create a simple logging system where each log level (INFO, WARNING, ERROR) has its own handler. If a handler cannot process a log message, it passes it to the next handler in the chain.
// Logger.js
class Logger {
constructor(nextLogger = null) {
this.nextLogger = nextLogger;
}
setNextLogger(nextLogger) {
this.nextLogger = nextLogger;
}
logMessage(level, message) {
if (this.canHandle(level)) {
this.write(message);
} else if (this.nextLogger) {
this.nextLogger.logMessage(level, message);
}
}
canHandle(level) {
throw new Error("This method should be overridden by subclasses");
}
write(message) {
throw new Error("This method should be overridden by subclasses");
}
}
class InfoLogger extends Logger {
canHandle(level) {
return level === 'INFO';
}
write(message) {
console.log(`Info: ${message}`);
}
}
class WarningLogger extends Logger {
canHandle(level) {
return level === 'WARNING';
}
write(message) {
console.warn(`Warning: ${message}`);
}
}
class ErrorLogger extends Logger {
canHandle(level) {
return level === 'ERROR';
}
write(message) {
console.error(`Error: ${message}`);
}
}
**Usage:**
```jsx
// main.js
const errorLogger = new ErrorLogger();
const warningLogger = new WarningLogger(errorLogger);
const infoLogger = new InfoLogger(warningLogger);
infoLogger.logMessage('INFO', 'This is an info message');
infoLogger.logMessage('WARNING', 'This is a warning message');
infoLogger.logMessage('ERROR', 'This is an error message');
Output:
<OutputBlock>
{`Info: This is an info message
Warning: This is a warning message
Error: This is an error message`}
</OutputBlock>
Problem Statement: You want to encapsulate requests as objects, allowing for parameterization of clients with different requests, queuing or logging requests, and supporting undoable operations.
Exercise:
Create a simple text editor that supports basic commands like InsertText, DeleteText, and Undo.
// Command.js
class Command {
execute() {
throw new Error("This method should be overridden by subclasses");
}
undo() {
throw new Error("This method should be overridden by subclasses");
}
}
class InsertTextCommand extends Command {
constructor(editor, text) {
this.editor = editor;
this.text = text;
this.backup = null;
}
execute() {
this.backup = this.editor.getText();
this.editor.insert(this.text);
}
undo() {
if (this.backup !== null) {
this.editor.setText(this.backup);
}
}
}
class DeleteTextCommand extends Command {
constructor(editor, length) {
this.editor = editor;
this.length = length;
this.backup = null;
}
execute() {
this.backup = this.editor.getText();
this.editor.delete(this.length);
}
undo() {
if (this.backup !== null) {
this.editor.setText(this.backup);
}
}
}
Usage:
// Editor.js
class Editor {
constructor() {
this.text = '';
this.history = [];
}
insert(text) {
this.text += text;
}
delete(length) {
this.text = this.text.slice(0, -length);
}
getText() {
return this.text;
}
setText(text) {
this.text = text;
}
executeCommand(command) {
command.execute();
this.history.push(command);
}
undoLastCommand() {
if (this.history.length > 0) {
const lastCommand = this.history.pop();
lastCommand.undo();
}
}
}
Usage:
// main.js
const editor = new Editor();
const insertCmd = new InsertTextCommand(editor, "Hello");
editor.executeCommand(insertCmd);
console.log(editor.getText()); // Output: Hello
const deleteCmd = new DeleteTextCommand(editor, 2);
editor.executeCommand(deleteCmd);
console.log(editor.getText()); // Output: Hel
editor.undoLastCommand();
console.log(editor.getText()); // Output: Hello
Problem Statement: You want to define a dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Exercise:
Create a simple weather station system where multiple displays (like temperature display and humidity display) update themselves whenever the weather data changes.
// Observer.js
class Subject {
constructor() {
this.observers = [];
}
registerObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notifyObservers() {
for (const observer of this.observers) {
observer.update(this.temperature, this.humidity);
}
}
}
class WeatherData extends Subject {
constructor() {
super();
this.temperature = 0;
this.humidity = 0;
}
measurementsChanged() {
this.notifyObservers();
}
setMeasurements(temperature, humidity) {
this.temperature = temperature;
this.humidity = humidity;
this.measurementsChanged();
}
}
Usage:
// Display.js
class Observer {
update(temperature, humidity) {
throw new Error("This method should be overridden by subclasses");
}
}
class CurrentConditionsDisplay extends Observer {
constructor(weatherData) {
super();
this.weatherData = weatherData;
this.weatherData.registerObserver(this);
}
update(temperature, humidity) {
console.log(`Current conditions: ${temperature}F degrees and ${humidity}% humidity`);
}
}
class StatisticsDisplay extends Observer {
constructor(weatherData) {
super();
this.weatherData = weatherData;
this.weatherData.registerObserver(this);
}
update(temperature, humidity) {
console.log(`Statistics: Avg/Max/Min temperature = 80.0/82.0/78.0F`);
}
}
Usage:
// main.js
const weatherData = new WeatherData();
const currentDisplay = new CurrentConditionsDisplay(weatherData);
const statisticsDisplay = new StatisticsDisplay(weatherData);
weatherData.setMeasurements(80, 65);
weatherData.setMeasurements(82, 70);
weatherData.setMeasurements(78, 90);
Output:
<OutputBlock>
{`Current conditions: 80F degrees and 65% humidity
Statistics: Avg/Max/Min temperature = 80.0/82.0/78.0F
Current conditions: 82F degrees and 70% humidity
Statistics: Avg/Max/Min temperature = 80.0/82.0/78.0F
Current conditions: 78F degrees and 90% humidity
Statistics: Avg/Max/Min temperature = 80.0/82.0/78.0F`}
</OutputBlock>
Now that you have a good understanding of how to apply behavioral patterns, you can explore more advanced topics such as design patterns in software architecture. These patterns provide a robust framework for designing scalable and maintainable systems.
Feel free to experiment with these patterns in your projects and expand your knowledge by reading more about design patterns in various programming languages and frameworks.