Chapter 18

コマンドパターン

Shinya Fujino
Shinya Fujino
2022.01.07に更新
このチャプターの目次

Commander にコマンドを送信することで、タスクを実行するメソッドを分離する


コマンドパターン

コマンドパターン (command pattern) を用いると、あるタスクを実行するオブジェクトと、そのメソッドを呼び出すオブジェクトを切り離すことができことができます。

たとえば、オンラインフードデリバリーのプラットフォームがあったとします。ユーザーは注文をしたり (place)、追跡したり (track)、キャンセルしたり (cancel) することができます。

class OrderManager() {
  constructor() {
    this.orders = []
  }

  placeOrder(order, id) {
    this.orders.push(id)
    return `You have successfully ordered ${order} (${id})`;
  }

  trackOrder(id) {
    return `Your order ${id} will arrive in 20 minutes.`
  }

  cancelOrder(id) {
    this.orders = this.orders.filter(order => order.id !== id)
    return `You have canceled your order ${id}`
  }
}

ここでは OrderManager クラスの placeOrdertrackOrdercancelOrder メソッドにアクセスすることができます。これらのメソッドを直接使用しても、JavaScript としてまったく問題はありません。

const manager = new OrderManager();

manager.placeOrder("Pad Thai", "1234");
manager.trackOrder("1234");
manager.cancelOrder("1234");

しかし、manager インスタンスのメソッドを直接呼び出すことにはデメリットがあります。それは、あるメソッドの名前をあとで変更することになったり、メソッドの機能が変更されたりした場合に発生します。

たとえば、placeOrder という名前を addOrder へと変更したとしましょう。このとき、placeOrder メソッドがコードベースのどこからも呼び出されないようにしなければなりませんが、これは大規模なアプリケーションでは非常に厄介なことです。

代わりに、manager オブジェクトからメソッドを切り離し、各コマンドに対応する個別のコマンド関数を作成します。

OrderManager クラスをリファクタリングしましょう。placeOrdercancelOrdertrackOrder メソッドの代わりに、execute という単一のメソッドをもつようにします。このメソッドは、与えられた任意のコマンドを実行します。

各コマンドは、OrderManagerorders にアクセスする必要があるため、これを最初の引数として渡します。

class OrderManager {
  constructor() {
    this.orders = [];
  }

  execute(command, ...args) {
    return command.execute(this.orders, ...args);
  }
}

OrderManager に与える 3 つの Command を作成します:

  • PlaceOrderCommand
  • CancelOrderCommand
  • TrackOrderCommand
class Command {
  constructor(execute) {
    this.execute = execute;
  }
}

function PlaceOrderCommand(order, id) {
  return new Command(orders => {
    orders.push(id);
    return `You have successfully ordered ${order} (${id})`;
  });
}

function CancelOrderCommand(id) {
  return new Command(orders => {
    orders = orders.filter(order => order.id !== id);
    return `You have canceled your order ${id}`;
  });
}

function TrackOrderCommand(id) {
  return new Command(() => `Your order ${id} will arrive in 20 minutes.`);
}

完璧です!これらのメソッドは、OrderManager のインスタンスと密結合していません。OrderManagerexecute メソッドを通して呼び出すことができる、疎結合化された関数となっています。

index.js
class OrderManager {
  constructor() {
    this.orders = [];
  }

  execute(command, ...args) {
    return command.execute(this.orders, ...args);
  }
}

class Command {
  constructor(execute) {
    this.execute = execute;
  }
}

function PlaceOrderCommand(order, id) {
  return new Command(orders => {
    orders.push(id);
    console.log(`You have successfully ordered ${order} (${id})`);
  });
}

function CancelOrderCommand(id) {
  return new Command(orders => {
    orders = orders.filter(order => order.id !== id);
    console.log(`You have canceled your order ${id}`);
  });
}

function TrackOrderCommand(id) {
  return new Command(() =>
    console.log(`Your order ${id} will arrive in 20 minutes.`)
  );
}

const manager = new OrderManager();

manager.execute(new PlaceOrderCommand("Pad Thai", "1234"));
manager.execute(new TrackOrderCommand("1234"));
manager.execute(new CancelOrderCommand("1234"));

Pros

コマンドパターンにより、ある操作を実行するオブジェクトから、メソッドを切り離すことができます。これは、特定の寿命をもつコマンドや、特定の時間にキューに入れられ実行されるようなコマンドを扱う場合に、より細かな制御を可能とします。

Cons

コマンドパターンのユースケースは非常に限られています。また、不要なボイラープレートをアプリケーションに追加してしまうことも少なくありません。


参考文献