🧾
デザインパターン:Commandパターン
これは何?
リクエストを、関連する情報を含む独立したオブジェクトにまとめる手法。
レストランの注文票のようなもの
「操作」のカプセル化
簡易コード
電気をつけたり消したりする
// Receiver
class Light {
turnOn() {
console.log("The light is on");
}
turnOff() {
console.log("The light is off");
}
}
// Command Interface
interface Command {
execute(): void;
}
// Concrete Commands
class TurnOnCommand implements Command {
constructor(private light: Light) {}
execute() {
this.light.turnOn();
}
}
class TurnOffCommand implements Command {
constructor(private light: Light) {}
execute() {
this.light.turnOff();
}
}
// Invoker
class RemoteControl {
submit(command: Command) {
command.execute();
}
}
// Client
class Client {
// Client decides which commands to execute at what time
public static main() {
const light = new Light(); // Receiver
const turnOnCommand = new TurnOnCommand(light); // Command
const turnOffCommand = new TurnOffCommand(light); // Command
const remote = new RemoteControl(); // Invoker
remote.submit(turnOnCommand); // turn the light on!
remote.submit(turnOffCommand); // turn the light off!
}
}
何が嬉しいのか
undo/redoの実装や、操作の遅延実行など、複雑な実装に対する拡張性が高い
問題
時系列的に↓こんな感じで実装しようとして困った
- タスク管理ツールを作成
- TaskUseCaseなどを使ってデータをゴニョゴニョ
- undoなどを実装
- TaskUseCase内にhistory的な概念を用意
- タスク管理ツールの見た目変更機能を実装 ← New!
- ChangeAppearanceUseCaseを作成
- もちろんこれにもundoを用意したい
- もちろんタスク更新の履歴とも合わせて操作順を管理した上でundoしたい
- 「あれ、操作履歴ってどこに書けば良いんだ…」
解決
「Commandパターンや!」
↓面倒なので見た目変更に関しては記載してないけど、Commandパターンを使ったundoの実装を記載してます。見た目変更用のUseCaseを追加すれば簡単に要件を満たせます
ReceiverクラスはUseCaseとして実装
// Commandインターフェース
interface Command {
execute(): void;
undo(): void;
}
// TaskUseCaseクラス (Receiver)
class TaskUseCase {
private tasks: string[] = [];
addTask(task: string) {
this.tasks.push(task);
}
removeTask() {
this.tasks.pop();
}
getTasks(): string[] {
return this.tasks;
}
}
// 具体的なCommandクラス
class AddTaskCommand implements Command {
private taskUseCase: TaskUseCase;
private task: string;
constructor(taskUseCase: TaskUseCase, task: string) {
this.taskUseCase = taskUseCase;
this.task = task;
}
execute(): void {
this.taskUseCase.addTask(this.task);
}
undo(): void {
this.taskUseCase.removeTask();
}
}
// Invokerクラス
class TaskInvoker {
private commandHistory: Command[] = [];
private taskUseCase: TaskUseCase;
constructor(taskUseCase: TaskUseCase) {
this.taskUseCase = taskUseCase;
}
executeCommand(command: Command) {
this.commandHistory.push(command);
command.execute();
}
undo() {
const command = this.commandHistory.pop();
if (command) {
command.undo();
}
}
printTasks() {
console.log("Current Tasks:", this.taskUseCase.getTasks());
}
}
// Clientコード
const taskUseCase = new TaskUseCase();
const invoker = new TaskInvoker(taskUseCase);
invoker.executeCommand(new AddTaskCommand(taskUseCase, 'Learn TypeScript'));
invoker.printTasks(); // ['Learn TypeScript']
invoker.executeCommand(new AddTaskCommand(taskUseCase, 'Unlearn TypeScript'));
invoker.printTasks(); // ['Learn TypeScript', 'Unlearn TypeScript']
// Undo操作
invoker.undo();
invoker.printTasks(); // ['Learn TypeScript']
その他の推しポイント
- 単一責任の原則を遂行できる
- オープンクローズドの原則を遂行できる
動く実装例
上のコードと同じ
参考文献
- フロントエンドのデザインパターン
コマンドパターン|フロントエンドのデザインパターン
Discussion