【TypeScript】コマンドパターンの型安全な実装 - 柔軟な操作管理と拡張性の確保

【TypeScript】コマンドパターンの型安全な実装 - 柔軟な操作管理と拡張性の確保

2024-11-10

2024-11-10

コマンドパターンの概要とTypeScriptでの型安全な実装

コマンドパターンは、操作をオブジェクトとしてカプセル化し、実行や取り消しを一貫して管理できるようにするデザインパターンです。TypeScriptでコマンドパターンを実装することで、各操作を型で管理し、柔軟で型安全な操作の実装が可能となります。この記事では、TypeScriptによるコマンドパターンの型安全な実装方法について詳しく解説します。

コマンドパターンの構造

コマンドパターンの構造は以下の要素で成り立っています:

  1. Commandインターフェース: 各コマンドに共通のインターフェースで、executeメソッドを定義します。
  2. ConcreteCommand(具象コマンド): Commandインターフェースを実装した具体的な操作。
  3. Invoker(呼び出し元): コマンドを管理・実行する役割を担い、特定のコマンドをexecuteメソッドで実行します。
  4. Receiver(受け手): 実際にコマンドの効果を受けるオブジェクトで、操作の対象です。 コマンドパターンは、操作をオブジェクトとして扱うことで、実行のほか取り消しや再実行も柔軟に実装できるメリットがあります。

TypeScriptでの基本的なコマンドパターンの実装

まず、シンプルなコマンドパターンの実装を見ていきましょう。ここでは、電気機器の電源操作をコマンドとして実装します。

// Commandインターフェース
interface Command {
  execute(): void;
}
// Receiverクラス
class Light {
  on() {
    console.log("ライトが点灯しました。");
  }
  off() {
    console.log("ライトが消灯しました。");
  }
}
// ConcreteCommandクラス(ライトを点灯するコマンド)
class LightOnCommand implements Command {
  private light: Light;
  constructor(light: Light) {
    this.light = light;
  }
  execute(): void {
    this.light.on();
  }
}
// ConcreteCommandクラス(ライトを消灯するコマンド)
class LightOffCommand implements Command {
  private light: Light;
  constructor(light: Light) {
    this.light = light;
  }
  execute(): void {
    this.light.off();
  }
}
// Invokerクラス
class RemoteControl {
  private command!: Command;
  setCommand(command: Command): void {
    this.command = command;
  }
  pressButton(): void {
    this.command.execute();
  }
}

このコードでは、Commandインターフェースを実装したLightOnCommandLightOffCommandが、それぞれライトの点灯と消灯を実行します。RemoteControlクラスはsetCommandメソッドでコマンドを設定し、pressButtonメソッドで実行します。

使用例

const light = new Light();
const lightOnCommand = new LightOnCommand(light);
const lightOffCommand = new LightOffCommand(light);
const remote = new RemoteControl();
remote.setCommand(lightOnCommand);
remote.pressButton();  // 出力: ライトが点灯しました。
remote.setCommand(lightOffCommand);
remote.pressButton();  // 出力: ライトが消灯しました。

このコードにより、リモコンでライトの点灯・消灯を切り替える操作ができるようになり、コマンドの設定により実行動作を変更することができます。

型安全なコマンドパターンの実装

TypeScriptでは、ジェネリクスを使ってコマンドパターンをさらに型安全にすることが可能です。たとえば、異なるパラメータや戻り値を持つコマンドを型定義する場合に、ジェネリクスを使用してコマンドの型を厳密に管理できます。

パラメータ付きの型安全なコマンドパターン

ここでは、音楽プレイヤーの操作を型安全なコマンドパターンで実装します。

// コマンドインターフェース(ジェネリクスを使用)
interface Command<T = void> {
  execute(payload: T): void;
}
// 音楽プレイヤーのReceiverクラス
class MusicPlayer {
  play(song: string) {
    console.log(`再生中: ${song}`);
  }
  stop() {
    console.log("音楽を停止しました。");
  }
}
// 楽曲を再生するコマンド
class PlayCommand implements Command<string> {
  private player: MusicPlayer;
  constructor(player: MusicPlayer) {
    this.player = player;
  }
  execute(song: string): void {
    this.player.play(song);
  }
}
// 再生を停止するコマンド
class StopCommand implements Command {
  private player: MusicPlayer;
  constructor(player: MusicPlayer) {
    this.player = player;
  }
  execute(): void {
    this.player.stop();
  }
}
// Invokerクラス
class MusicController {
  private command!: Command<any>;
  setCommand(command: Command<any>): void {
    this.command = command;
  }
  pressButton(payload?: any): void {
    this.command.execute(payload);
  }
}

この例では、Commandインターフェースにジェネリクス<T>を用いて、コマンドに必要な引数の型を定義しています。PlayCommandは再生する曲名の引数としてstring型を持ち、StopCommandは引数なしで実行されます。

使用例

const player = new MusicPlayer();
const playCommand = new PlayCommand(player);
const stopCommand = new StopCommand(player);
const controller = new MusicController();
controller.setCommand(playCommand);
controller.pressButton("Imagine - John Lennon");  // 出力: 再生中: Imagine - John Lennon
controller.setCommand(stopCommand);
controller.pressButton();  // 出力: 音楽を停止しました。

ジェネリクスを使用することで、コマンドに異なる型の引数を柔軟に渡せるようになり、型安全な操作が可能です。

複数の操作履歴を管理するコマンドパターン

コマンドパターンをさらに拡張し、実行した操作の履歴を保持して取り消し機能を追加することも可能です。次の例では、コマンドの履歴を管理し、取り消しを実装しています。

// UndoableCommandインターフェース
interface UndoableCommand<T = void> extends Command<T> {
  undo(): void;
}
// 音量の上げ下げを管理するクラス
class VolumeControl {
  private volume = 50;
  increase() {
    this.volume = Math.min(100, this.volume + 10);
    console.log(`音量: ${this.volume}`);
  }
  decrease() {
    this.volume = Math.max(0, this.volume - 10);
    console.log(`音量
: ${this.volume}`);
  }
}
// 音量を上げるコマンド
class VolumeUpCommand implements UndoableCommand {
  private volumeControl: VolumeControl;
  constructor(volumeControl: VolumeControl) {
    this.volumeControl = volumeControl;
  }
  execute(): void {
    this.volumeControl.increase();
  }
  undo(): void {
    this.volumeControl.decrease();
  }
}
// 音量を下げるコマンド
class VolumeDownCommand implements UndoableCommand {
  private volumeControl: VolumeControl;
  constructor(volumeControl: VolumeControl) {
    this.volumeControl = volumeControl;
  }
  execute(): void {
    this.volumeControl.decrease();
  }
  undo(): void {
    this.volumeControl.increase();
  }
}
// コマンド履歴を管理するInvokerクラス
class RemoteControlWithUndo {
  private history: UndoableCommand[] = [];
  executeCommand(command: UndoableCommand): void {
    command.execute();
    this.history.push(command);
  }
  undoCommand(): void {
    const command = this.history.pop();
    if (command) {
      command.undo();
    }
  }
}

使用例

const volumeControl = new VolumeControl();
const volumeUp = new VolumeUpCommand(volumeControl);
const volumeDown = new VolumeDownCommand(volumeControl);
const remote = new RemoteControlWithUndo();
remote.executeCommand(volumeUp); // 出力: 音量: 60
remote.executeCommand(volumeDown); // 出力: 音量: 50
remote.undoCommand(); // 出力: 音量: 60
remote.undoCommand(); // 出力: 音量: 50

この例では、UndoableCommandインターフェースを使ってundoメソッドを追加し、各操作に対する取り消し機能を実装しています。RemoteControlWithUndoクラスが履歴を管理し、実行したコマンドを取り消せるようになっています。

まとめ

TypeScriptでコマンドパターンを実装することで、操作のオブジェクト化と柔軟な管理が可能になります。ジェネリクスやインターフェースを活用することで、異なる引数や戻り値の型に対応し、履歴管理や取り消しなどの機能も型安全に実装できます。コマンドパターンを導入することで、コードの柔軟性と拡張性を高め、保守性を向上させましょう。

Recommend