🧾

デザインパターン:Commandパターン

2024/07/19に公開

これは何?

リクエストを、関連する情報を含む独立したオブジェクトにまとめる手法。

レストランの注文票のようなもの

「操作」のカプセル化

簡易コード

電気をつけたり消したりする

// 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の実装や、操作の遅延実行など、複雑な実装に対する拡張性が高い

問題

時系列的に↓こんな感じで実装しようとして困った

  1. タスク管理ツールを作成
    1. TaskUseCaseなどを使ってデータをゴニョゴニョ
  2. undoなどを実装
    1. TaskUseCase内にhistory的な概念を用意
  3. タスク管理ツールの見た目変更機能を実装 ← New!
    1. ChangeAppearanceUseCaseを作成
    2. もちろんこれにもundoを用意したい
    3. もちろんタスク更新の履歴とも合わせて操作順を管理した上でundoしたい
  4. 「あれ、操作履歴ってどこに書けば良いんだ…」

解決

「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']

その他の推しポイント

  • 単一責任の原則を遂行できる
  • オープンクローズドの原則を遂行できる

動く実装例

上のコードと同じ

https://www.typescriptlang.org/ja/play?#code/PTAEGEHsFtoQwHYBNAlDIZ4ZD9DIH4ZCrDIcoYtBOhgCgBLBAFwFMAnAMzgGMqIZ5lQBvY0X0KgB5VGAV2oAKAJQAuUADdIpJAG4efEckhTZCpaoC+xYiFAAVOAGcA1gFULVcJaqB6hkCXDIVDiASsKqk5tJLEjAA2lhZmlrb2jvZcarwADjT+cNSgFFEWshYUKQgA5gDaALqgALygpaoJoHBISObW4pnWOXnkBZLxfL0ZABakFgB0rVYjiSIW-S1Rkqq9hrU0VNCQAU1WUj19vBSDI2MTkIlSC3xLvQVUFJsW2qC5+cVl3Lu8KxQiNAgDQ6NZc68QxLEyAd0VAMrygBC3QBWDFBYIgkG4SKFwqAAIINTbw9hIUCkaCJEKrKiUCI4xE7JIpORpFhjOwOJyyTaM2JUIGgZKpdJjdrPGq9RiQBBPESMCiQGizaxs5mRWUxJwAGgyUX5nW6b3e+3+DKVcUq+qZ9k5vV1hyiFTV1k5lz4gmEYioD10eO1uwtAMVJqow3qjSiLQO3qs81q9t4GiQWhk8kU7tq5pDxvZwxWaw2QfDi2IoLAAEkEAorLRkcEwhYIpsiyXaFSuTS6aBhQjkAAJIaSmgAT1kFOQpWt1Vq3NpvKicvsLMnBo5RiFIrFEqlMuivpnPvZWqTfC9qac1oPpojC4dQlE1AHSHErdx-bYiJ37z+IzviM7uSlPeGk2mt8fZAc3ed9kGGR1LxdYDgTPKNNG2D0+mFUUKBbQC8SNENQKQT9ux-RITjOXdeFIOhPGw58Xz4bDhmjWMzQuU9R3yW4sgQ4i0NFSBiWGEJIAKcQACJwG+FZKAVcZpEE1V91nX1hmuVjrHuSRoNAEEjBMcAQlIUkKEAZoYsEASYZgiXVDjxYSoECoAB3CSpyg1RkNyfFi0gUsaGtay7JrNyPLXBzwzIPzaHAi9nWvcRvIxLEokiizVQAcgAGSoOAfjMHtEioABlRgUkSChEtU1RyDrGhfxYu4zlAEwihStKMtMLLcvy0hCsSkpgvKsKnSvdCotsmLA2seK5PZJKbAQYl0t+ZrsrygqipK7r3NC7lKGq+ZarAerUtmzKFrajrJumxq5paxb2qKrrjDAKaY0AWZNAB15VaPNo+DwzKtaKo2pTxhquqGoO+bWqWzriCAA

参考文献

Tokyo, inc. Engineers

Discussion