Design Patterns in Angular: Command Method Design Pattern
The Command Design Pattern is a behavioral design pattern that turns a request into a stand-alone object called a command. With the help of this pattern, you can capture each component of a request, including the object that owns the method, the parameters for the method, and the method itself
In Angular, this pattern can be implemented to encapsulate user actions or commands, making the application easier to extend, test, and maintain.
The key components of the Command Design Pattern are:
Command: This is an interface or abstract class that declares the method execute()
. Concrete command classes implement this interface and define the specific actions to be taken when the command is executed.
ConcreteCommand: A subclass of Command
, this class defines the binding between a receiver object and an action. It implements the execute()
method to invoke the appropriate operation(s) on the receiver.
Client: The client creates a ConcreteCommand
object and sets the receiver for the command. It then passes the command to the invoker.
Invoker: The invoker is responsible for initiating the request (or command). It stores the command and can call the execute()
method at a later time, either on demand or in response to some event or action.
Receiver: The receiver is the object that knows how to perform the action to satisfy the request. It holds the business logic that needs to be executed when the command is triggered.
Invoker’s Request History (Optional): Sometimes the invoker might keep track of the commands in a history (queue or stack) for purposes like undo/redo or retry.
Let’s implement a Text Editor example where the user can perform actions like bold, italic, and underline. We’ll follow the SOLID principles to make sure the design is clean, scalable, and maintainable.
Project Structure
src/
├── app/
│ ├── app.component.ts
│ ├── app.component.html
│ ├── services/
│ │ ├── text-editor.service.ts
│ │ ├── command-invoker.service.ts
│ ├── commands/
│ │ ├── command.interface.ts
│ │ ├── bold-command.ts
│ │ ├── italic-command.ts
│ │ ├── underline-command.ts
│ ├── models/
│ │ ├── text-state.ts
│ ├── app.module.ts
1. Text State Model (SRP)
This model will hold the current state of the text (e.g., with added styles).
export interface TextState {
content: string;
isBold: boolean;
isItalic: boolean;
isUnderlined: boolean;
}
2. Text Editor Service (SRP)
This service manages the current text content and its styles.
import { Injectable } from '@angular/core';
import { TextState } from '../models/text-state';
@Injectable({
providedIn: 'root',
})
export class TextEditorService {
private state: TextState = { content: '', isBold: false, isItalic: false, isUnderlined: false };
getState(): TextState {
return { ...this.state };
}
setState(state: TextState): void {
this.state = state;
}
applyBold(): void {
this.state.isBold = !this.state.isBold;
}
applyItalic(): void {
this.state.isItalic = !this.state.isItalic;
}
applyUnderline(): void {
this.state.isUnderlined = !this.state.isUnderlined;
}
updateContent(content: string): void {
this.state.content = content;
}
}
3. Command Interface (ISP)
The command interface defines the execute()
and undo()
methods that all commands will implement.
export interface Command {
execute(): void;
undo(): void;
}
4. Concrete Command Implementations
Each formatting action (bold, italic, underline) will be encapsulated as a separate command.
Bold Command
import { Command } from './command.interface';
import { TextEditorService } from '../services/text-editor.service';
export class BoldCommand implements Command {
private previousState: boolean;
constructor(private textEditor: TextEditorService) {}
execute(): void {
this.previousState = this.textEditor.getState().isBold;
this.textEditor.applyBold();
}
undo(): void {
if (this.previousState !== undefined) {
this.textEditor.setState({
...this.textEditor.getState(),
isBold: this.previousState,
});
}
}
}
Italic Command
import { Command } from './command.interface';
import { TextEditorService } from '../services/text-editor.service';
export class ItalicCommand implements Command {
private previousState: boolean;
constructor(private textEditor: TextEditorService) {}
execute(): void {
this.previousState = this.textEditor.getState().isItalic;
this.textEditor.applyItalic();
}
undo(): void {
if (this.previousState !== undefined) {
this.textEditor.setState({
...this.textEditor.getState(),
isItalic: this.previousState,
});
}
}
}
Underline Command
import { Command } from './command.interface';
import { TextEditorService } from '../services/text-editor.service';
export class UnderlineCommand implements Command {
private previousState: boolean;
constructor(private textEditor: TextEditorService) {}
execute(): void {
this.previousState = this.textEditor.getState().isUnderlined;
this.textEditor.applyUnderline();
}
undo(): void {
if (this.previousState !== undefined) {
this.textEditor.setState({
...this.textEditor.getState(),
isUnderlined: this.previousState,
});
}
}
}
5. Command Invoker (DIP)
The invoker is responsible for storing and invoking commands. It also handles undo/redo functionality.
import { Injectable } from '@angular/core';
import { Command } from '../commands/command.interface';
@Injectable({
providedIn: 'root',
})
export class CommandInvokerService {
private history: Command[] = [];
private redoStack: Command[] = [];
execute(command: Command): void {
command.execute();
this.history.push(command);
this.redoStack = [];
}
undo(): void {
const command = this.history.pop();
if (command) {
command.undo();
this.redoStack.push(command);
}
}
redo(): void {
const command = this.redoStack.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
}
6. Component (SRP)
The component allows the user to interact with the text editor and perform actions like bold, italic, or underline
import { Component } from '@angular/core';
import { CommandInvokerService } from './services/command-invoker.service';
import { TextEditorService } from './services/text-editor.service';
import { BoldCommand } from './commands/bold-command';
import { ItalicCommand } from './commands/italic-command';
import { UnderlineCommand } from './commands/underline-command';
@Component({
selector: 'app-root',
template: `
<textarea [(ngModel)]="textContent" (ngModelChange)="onContentChange()"></textarea>
<br />
<button (click)="toggleBold()">Bold</button>
<button (click)="toggleItalic()">Italic</button>
<button (click)="toggleUnderline()">Underline</button>
<br />
<button (click)="undo()">Undo</button>
<button (click)="redo()">Redo</button>
`,
})
export class AppComponent {
textContent = '';
constructor(
private textEditor: TextEditorService,
private commandInvoker: CommandInvokerService
) {}
onContentChange(): void {
this.textEditor.updateContent(this.textContent);
}
toggleBold(): void {
const command = new BoldCommand(this.textEditor);
this.commandInvoker.execute(command);
}
toggleItalic(): void {
const command = new ItalicCommand(this.textEditor);
this.commandInvoker.execute(command);
}
toggleUnderline(): void {
const command = new UnderlineCommand(this.textEditor);
this.commandInvoker.execute(command);
}
undo(): void {
this.commandInvoker.undo();
}
redo(): void {
this.commandInvoker.redo();
}
}
Explanation of the Code
TextEditorService: Manages the text content and its formatting states (bold, italic, underline). It can apply and undo each of these styles.
Command Pattern: Each action (BoldCommand
, ItalicCommand
, UnderlineCommand
) is a separate command that modifies the text editor’s state.
CommandInvokerService: Responsible for executing commands and keeping track of the history for undo and redo operations.
Component: Provides an interface for the user to interact with the text editor, and uses the CommandInvokerService
to execute commands.
When to use the Command Design Pattern
Decoupling is Needed:
- In order to separate the requester making the request (the sender) from the object executing it, use the Command Pattern.
- Your code will become more expandable and adaptable as a result.
Undo/Redo Functionality is Required:
- If you need to support undo and redo operations in your application, the Command Pattern is a good fit.
- Each command can encapsulate an operation and its inverse, making it easy to undo or redo actions.
Support for Queues and Logging:
- If you want to maintain a history of commands, log them, or even put them in a queue for execution, the Command Pattern provides a structured way to achieve this.
Dynamic Configuration:
- When you need the ability to dynamically configure and assemble commands at runtime, the Command Pattern allows for flexible composition of commands.