Javaのコマンドパターン:柔軟なコマンド実行を実現する
別名
- アクション
- トランザクション
コマンドデザインパターンの目的
コマンドデザインパターンは、Javaプログラミングで使用される振る舞いパターンです。リクエストをオブジェクトとしてカプセル化し、クライアントをキュー、リクエスト、および操作でパラメータ化できるようにします。このパターンは、元に戻せる操作もサポートし、コマンドの管理と実行の柔軟性を高めます。
現実世界の例を用いたコマンドパターンの詳細な説明
現実世界の例
中央アプリケーションを介して照明、サーモスタット、セキュリティカメラなどのデバイスを制御できるスマートホームシステムを想像してみてください。これらのデバイスを操作するための各コマンドはオブジェクトとしてカプセル化され、システムは必要に応じてコマンドをキューに入れ、順番に実行し、元に戻すことができます。このアプローチは、制御ロジックをデバイスの実装から分離し、コアアプリケーションを変更せずに新しいデバイスや機能を簡単に追加できるようにします。この柔軟性と機能性は、Javaプログラミングにおけるコマンドデザインパターンの実際的な適用を示しています。
分かりやすく言うと
リクエストをコマンドオブジェクトとして保存することで、アクションを実行したり、後で元に戻したりすることができます。
Wikipediaによると
オブジェクト指向プログラミングでは、コマンドパターンは、アクションを実行したり、後でイベントをトリガーしたりするために必要なすべての情報をカプセル化するためにオブジェクトを使用する振る舞いデザインパターンです。
Javaにおけるコマンドパターンのプログラム例
コマンドパターンでは、オブジェクトを使用して、アクションを実行したり、後でイベントをトリガーしたりするために必要なすべての情報がカプセル化されます。このパターンは、アプリケーションで元に戻す機能を実装するのに特に役立ちます。
この例では、`Wizard`が`Goblin`に呪文を唱えます。各呪文は、実行および元に戻すことができるコマンドオブジェクトであり、Javaのコマンドパターンのコア原則を示しています。呪文はゴブリンに1つずつ実行されます。最初の呪文はゴブリンを縮小し、2番目の呪文はゴブリンを不可視にします。その後、魔法使いは呪文を1つずつ逆にします。ここでの各呪文は、元に戻すことができるコマンドオブジェクトです。
`Wizard`クラスから始めましょう。
@Slf4j
public class Wizard {
private final Deque<Runnable> undoStack = new LinkedList<>();
private final Deque<Runnable> redoStack = new LinkedList<>();
public Wizard() {
}
public void castSpell(Runnable runnable) {
runnable.run();
undoStack.offerLast(runnable);
}
public void undoLastSpell() {
if (!undoStack.isEmpty()) {
var previousSpell = undoStack.pollLast();
redoStack.offerLast(previousSpell);
previousSpell.run();
}
}
public void redoLastSpell() {
if (!redoStack.isEmpty()) {
var previousSpell = redoStack.pollLast();
undoStack.offerLast(previousSpell);
previousSpell.run();
}
}
@Override
public String toString() {
return "Wizard";
}
}
次に、呪文の`Target`である`Goblin`があります。
@Slf4j
@Getter
@Setter
public abstract class Target {
private Size size;
private Visibility visibility;
public void printStatus() {
LOGGER.info("{}, [size={}] [visibility={}]", this, getSize(), getVisibility());
}
public void changeSize() {
var oldSize = getSize() == Size.NORMAL ? Size.SMALL : Size.NORMAL;
setSize(oldSize);
}
public void changeVisibility() {
var visible = getVisibility() == Visibility.INVISIBLE
? Visibility.VISIBLE : Visibility.INVISIBLE;
setVisibility(visible);
}
}
public class Goblin extends Target {
public Goblin() {
setSize(Size.NORMAL);
setVisibility(Visibility.VISIBLE);
}
@Override
public String toString() {
return "Goblin";
}
}
最後に、`Wizard`が呪文を唱える完全な例を示すことができます。
public static void main(String[] args) {
var wizard = new Wizard();
var goblin = new Goblin();
goblin.printStatus();
wizard.castSpell(goblin::changeSize);
goblin.printStatus();
wizard.castSpell(goblin::changeVisibility);
goblin.printStatus();
wizard.undoLastSpell();
goblin.printStatus();
wizard.undoLastSpell();
goblin.printStatus();
wizard.redoLastSpell();
goblin.printStatus();
wizard.redoLastSpell();
goblin.printStatus();
}
プログラムの出力は次のとおりです
20:13:38.406 [main] INFO com.iluwatar.command.Target -- Goblin, [size=normal] [visibility=visible]
20:13:38.409 [main] INFO com.iluwatar.command.Target -- Goblin, [size=small] [visibility=visible]
20:13:38.409 [main] INFO com.iluwatar.command.Target -- Goblin, [size=small] [visibility=invisible]
20:13:38.409 [main] INFO com.iluwatar.command.Target -- Goblin, [size=small] [visibility=visible]
20:13:38.409 [main] INFO com.iluwatar.command.Target -- Goblin, [size=normal] [visibility=visible]
20:13:38.409 [main] INFO com.iluwatar.command.Target -- Goblin, [size=small] [visibility=visible]
20:13:38.409 [main] INFO com.iluwatar.command.Target -- Goblin, [size=small] [visibility=invisible]
Javaでコマンドパターンを使用する場合
コマンドデザインパターンは、アクションでオブジェクトをパラメータ化する必要がある場合、元に戻す操作をサポートする場合、またはプリミティブな操作に基づいて構築された高レベルの操作を中心にシステムを構築する場合に適用できます。 GUIボタン、データベース トランザクション、マクロ記録でよく使用されます。
以下の場合にコマンドパターンを使用します
- 実行するアクションでオブジェクトをパラメータ化し、手続き型言語で見られるコールバックのオブジェクト指向の代替手段を提供します。コマンドは登録して後で実行できます。
- 異なる時間にリクエストを指定、キューイング、および実行し、コマンドが元のリクエストとは独立して存在し、プロセス間で転送されることさえできるようにします。
- コマンドの実行操作が状態を保存し、以前のアクションを元に戻すための実行解除操作を含む、元に戻す機能をサポートします。これにより、履歴リストを維持することで、無制限の元に戻す操作とやり直し操作が可能になります。
- システムクラッシュ後に再適用するために変更をログに記録します。コマンドインターフェースにロード操作とストア操作を追加することにより、変更の永続的なログを維持し、このログからコマンドをリロードして再実行することにより回復できます。
- トランザクションベースのシステムで一般的な、プリミティブな操作に基づいて構築された高レベルの操作を中心にシステムを構築します。コマンドパターンは、操作を呼び出して拡張するための共通インターフェースを提供することにより、トランザクションをモデル化します。
- リクエストの履歴を保持します。
- コールバック機能を実装します。
- 元に戻す機能を実装します。
Javaにおけるコマンドパターンの実際の適用例
- デスクトップアプリケーションのGUIボタンとメニュー項目。
- ロールバックをサポートするデータベースシステムとトランザクションシステムの操作。
- テキストエディタやスプレッドシートなどのアプリケーションでのマクロ記録。
- java.lang.Runnable
- org.junit.runners.model.Statement
- Netflix Hystrix
- javax.swing.Action
コマンドパターンの利点と欠点
利点
- 操作を呼び出すオブジェクトと、操作の実行方法を知るオブジェクトを分離します。
- 既存のクラスを変更する必要がないため、新しいコマンドを簡単に追加できます。
- 一連のコマンドを複合コマンドにまとめることができます。
欠点
- 個々のコマンドごとにクラスの数が増加します。
- 送信者と受信者の間に複数のレイヤーを追加することにより、設計が複雑になる可能性があります。
関連するJavaデザインパターン
- コンポジット:コンポジットパターンを使用してコマンドを構成し、マクロコマンドを作成できます。
- メモ:元に戻すメカニズムを実装するために使用できます。
- オブザーバー:コマンドをトリガーする変更について、パターンを観察できます。