command-pattern について
コマンドオブジェクトによって、操作の実行と呼び出し元を分離する JavaScript のデザインパターンを解説します。コマンドのキュー管理などが必要な開発者向けで、導入が不要な場合も説明します。
主な機能
- 01呼び出し元と操作ロジックの分離
- 02コマンドオブジェクトによる操作のカプセル化
- 03単一の execute メソッドによる実行
- 04キュー登録や指定時刻の実行
活用シーン
- 01注文操作を後で実行するためにキューへ登録
- 02呼び出し元を変えずに操作の実装を変更
- 03単純な単発呼び出しではパターンを避ける
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill command-patternインストール後、Claude Code で/command-patternを入力して読み込まれたか確認します。
詳細
コマンドパターン
コマンドパターンを使うと、特定のタスクを実行するオブジェクトと、そのメソッドを呼び出すオブジェクトを_分離_できます。
オンラインのフードデリバリープラットフォームがあるとします。ユーザーは注文の作成、追跡、キャンセルを行えます。
使用する場合
- 操作を呼び出すオブジェクトと、それを実行するオブジェクトを分離する必要がある場合に使用します
- コマンドに一定の有効期間が必要な場合や、コマンドをキューに入れて特定のタイミングで実行する場合に便利です
使用しない場合
- 元に戻す/やり直す機能、キューイング、ロギングが不要な、単純で一度きりの操作の場合
- 直接関数を呼び出すだけで十分に明確で、抽象化を追加してもメリットより複雑さが増す場合
- 操作の数が少なく、コマンドの仕組みを導入すると過剰設計になる場合
手順
- アクションをカプセル化する
executeメソッドを持つCommandクラスを作成します - 直接メソッドを呼び出す代わりに、コマンドオブジェクトをマネージャーの単一の
executeメソッドに渡します - このパターンは、不必要な定型コードが増える可能性があるため、シンプルなJavaScriptアプリケーションでは控えめに使用します
詳細
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)
return `You have canceled your order ${id}`
}
}
OrderManagerクラスでは、placeOrder、trackOrder、cancelOrderメソッドを利用できます。これらのメソッドを直接使うのも、JavaScriptとしてまったく問題ありません。
const manager = new OrderManager();
manager.placeOrder("Pad Thai", "1234");
manager.trackOrder("1234");
manager.cancelOrder("1234");
しかし、managerインスタンスのメソッドを直接呼び出すことにはデメリットがあります。後から特定のメソッド名を変更したり、メソッドの機能を変更したりすることがあるかもしれません。
たとえば、placeOrderの代わりにaddOrderと呼ぶように変更したとします。そうなると、コードベースのどこからもplaceOrderメソッドが呼び出されていないことを確認する必要があり、大規模なアプリケーションでは非常に難しい場合があります。その代わりに、メソッドをmanagerオブジェクトから分離し、各コマンドに対応する個別のコマンド関数を作成します。
OrderManagerクラスをリファクタリングしましょう。placeOrder、cancelOrder、trackOrderメソッドを持つ代わりに、executeという単一のメソッドを持たせます。このメソッドは、渡された任意のコマンドを実行します。
各コマンドはマネージャーのordersにアクセスできるようにします。これを各コマンドの最初の引数として渡します。
class OrderManager {
constructor() {
this.orders = [];
}
execute(command, ...args) {
return command.execute(this.orders, ...args);
}
}
注文マネージャー用に、次の3つのCommandを作成する必要があります。
PlaceOrderCommandCancelOrderCommandTrackOrderCommand
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) => {
const index = orders.indexOf(id);
if (index > -1) orders.splice(index, 1);
return `You have canceled your order ${id}`;
});
}
function TrackOrderCommand(id) {
return new Command(() => `Your order ${id} will arrive in 20 minutes.`);
}
完了です。メソッドはOrderManagerインスタンスに直接結び付けられるのではなく、個別に分離された関数になりました。これらの関数は、OrderManagerで利用できるexecuteメソッドを通じて呼び出せます。
メリット
コマンドパターンを使うと、操作を実行するオブジェクトからメソッドを分離できます。有効期間のあるコマンドや、キューに入れて特定のタイミングで実行するコマンドを扱う場合、より細かな制御が可能になります。
デメリット
コマンドパターンのユースケースはかなり限られており、アプリケーションに不必要な定型コードを追加することも少なくありません。
ソース
参考資料
- コマンドデザインパターン - SourceMaking
- コマンドパターン - Refactoring Guru
- コマンドパターン - Carlos Caballero