blog

デザインパターン研究ノート:コマンドパターン

日常生活では、スイッチを使って照明や換気扇などの電化製品の電源を入れたり切ったりします。スイッチは要求の送り手と解釈することができ、ライトは要求の最も一般的な受け手であり処理装置です。 スイッチとライ...

Oct 21, 2020 · 10 min. read
シェア

概要

はじめに

日常生活において、スイッチは照明や換気扇などの電化製品のオン・オフを制御するために使用されます。スイッチは要求の送り手であり、ライトは要求の最も赤い受け手であり、プロセッサであると理解することができます。 スイッチとライトの間には直接的な結合がなく、両者はワイヤで接続されているため、異なるワイヤを異なる要求の受け手に接続することができ、ワイヤを交換するだけで、同じ送り手が異なる受け手に対応することができます。

ソフトウェア開発は、多くの場合、特定のオブジェクトに要求を送信する必要がありますが、特定の受信者が誰であるかを知らない、また、要求された操作がどれであるかを知らない、この時点で私は疎結合の方法でソフトウェアを設計したいので、要求の送信者と要求の受信者が互いの間の結合を排除するために、オブジェクト間の呼び出し関係がより柔軟であるように、柔軟に要求の受信者だけでなく、要求された操作を指定することができます、コマンドパターンは、ソフトウェアを設計するために使用することができます。この時点で、設計のためにコマンドパターンを使用することができます。

コマンドパターンは、リクエストの送信者と受信者から完全に切り離すことができ、送信者と受信者の間に直接の参照関係はありません、要求を送信するオブジェクトは、要求を送信する方法だけを知っている必要があり、要求を完了する方法を知っている必要はありません。

定義

コマンドモード: リクエストをオブジェクトにカプセル化し、異なるリクエストを使ってクライアントをパラメータ化したり、リクエストをキューに入れたり、ログに記録したり、取り消し可能な操作をサポートします。

コマンドパターンは、アクションパターンやトランザクションパターンとも呼ばれるオブジェクト行動パターンです。

構造

役割

  • コマンド(Command): 抽象コマンドクラスは一般的に、リクエストを実行するための execute()メソッドが宣言された抽象クラスまたはインターフェースです。
  • ConcreteCommand: 抽象コマンドクラスで宣言されたメソッドを実装し、具象レシーバオブジェクトに対応し、レシーバオブジェクトのアクションをバインドし、execute()メソッドの実装時にレシーバオブジェクトの関連するアクションを呼び出します。
  • 受信者:リクエストに関連する操作を実行し、具体的にはリクエストの業務処理を実現します。

典型的な実装

ステップ

  • 抽象コマンドクラスの定義:リクエストを実行するためのメソッドを定義します。
  • 呼び出し元を定義する:呼び出しメソッド内に具象コマンドへの呼び出しを含め、抽象コマンドへの参照も含めます。
  • 受信者の定義:リクエストを受信するためのビジネスメソッドを定義します。
  • 具象コマンドクラスの定義:抽象コマンドクラスを継承/実装し、リクエストメソッドが実行され、受信者に転送されるreceiveメソッドを実装します。

抽象コマンドクラス

インターフェースとして実装されています:

interface Command
{
 void execute();
}

呼び出し元

class Invoker
{
 private Command command;
 public Invoker(Command command)
 {
 this.command = command;
 }
 public void call()
 {
 System.out.println("インボーカー操作");
 command.execute();
 }
}

レシーバー

class Receiver
{
 public void action()
 {
 System.out.println("レシーバー操作");
 }
}

ここでのレシーバーのアクションは1つだけで、受信方法を示します。

特定のコマンドクラス

class ConcreteCommand implements Command
{
 private Receiver receiver = new Receiver();
 @Override
 public void execute()
 {
 receiver.action();
 }
}

具体的なコマンド・クラスは、executeでレシーバーを呼び出すために、レシーバーへの参照を含む必要があります。

クライアント

public static void main(String[] args) 
{
 Invoker invoker = new Invoker(new ConcreteCommand());
 invoker.call();
}

出力は以下の通り:

ボタンのカスタムファンクションキー設定は、コマンドモードを使用して、ユーザーが必要に応じて機能を最小/最大/閉じるように設定できます。

デザインは以下の通り:

  • 抽象コマンドクラス: Command
  • 抽象コマンドクラス:コマンド
  • 呼び出し元:ボタン
  • レシーバー:MinimizeHandler+MaximizeHandler+CloseHandler

まず、executeメソッドだけを含むインターフェースとして実装された抽象コマンドクラスを設計します:

interface Command
{
 void execute();
}

次に、抽象コマンドへの参照を含む呼び出し元クラスです:

class Button
{
 private Command command;
 public Button(Command command)
 {
 this.command = command;
 }
 public void onClick()
 {
 System.out.println("ボタンがクリックされた");
 command.execute();
 }
}

次にレシーバークラス:

class MinimizeHandler
{
 public void handle()
 {
 System.out.println("最小化");
 }
}
class MaximizeHandler
{
 public void handle()
 {
 System.out.println("最大化");
 }
}
class CloseHandler
{
 public void handle()
 {
 System.out.println("閉じる");
 }
}

最後に、具体的なコマンドクラスは、受信機メンバの包含に対応することができ、実行を実装し、メソッドの受信機に転送します:

class MinimizeCommand implements Command
{
 private MinimizeHandler handler = new MinimizeHandler();
 @Override
 public void execute()
 {
 handler.handle();
 }
}
class MaximizeCommand implements Command
{
 private MaximizeHandler handler = new MaximizeHandler();
 @Override
 public void execute()
 {
 handler.handle();
 }
}
class CloseCommand implements Command
{
 private CloseHandler handler = new CloseHandler();
 @Override
 public void execute()
 {
 handler.handle();
 }
}

テストクラス:

public static void main(String[] args) 
{
 Button button = new Button(new MinimizeCommand());
 button.onClick();
 button = new Button(new MaximizeCommand());
 button.onClick();
 button = new Button(new CloseCommand());
 button.onClick();
}

出力:

コマンドキュー

時には、複数の要求をキューに、要求の送信者が完全な要求を送信するときに、複数の要求受信機は、応答を生成するために、これらの要求受信機は、1つずつ要求のビジネスメソッドの処理を完了するために実行されます必要です。この形式は、コマンドキューを介して達成することができる、コマンドキューの実装は非常に簡単ですが、一般的にクラスが複数のコマンドオブジェクトを格納するための責任があるCommandQueueと呼ばれるクラスを追加することです、別のコマンドオブジェクトは、上記の例のように、別の要求の受信者に対応することができますCommandQueueコマンドキュークラスを増やすことができます:

class CommandQueue
{
 private ArrayList<Command> commands = new ArrayList<>();
 public void add(Command command)
 {
 commands.add(command);
 }
 public void remove(Command command)
 {
 commands.remove(command);
 }
 public void execute()
 {
 System.out.println("コマンドのバッチ実行」)。;
 commands.forEach(Command::execute);
 }
}

次に呼び出し側のクラス Button を変更します:

class Button
{
 private CommandQueue queue;
 public Button(CommandQueue queue)
 {
 this.queue = queue;
 }
 public void onClick()
 {
 System.out.println("ボタンがクリックされた");
 queue.execute();
 }
}

最後に、コマンド・キューを定義し、呼び出し元のコンストラクタ・メソッドまたはセッターにパラメータとして渡すのはクライアントです:

public static void main(String[] args) 
{
 CommandQueue queue = new CommandQueue();
 queue.add(new MinimizeCommand());
 queue.add(new MaximizeCommand());
 queue.add(new CloseCommand());
 Button button = new Button(queue);
 button.onClick();
}

出力は以下の通り:

アンドゥとリドゥ

コマンドモードを使って、加算、取り消し、やり直し機能を実装した簡単な電卓を設計してください。

デザインは以下の通り:

  • 特定のコマンドクラス:MinimizeCommand+MaximizeCommand+CloseCommand
  • 抽象コマンドクラス:コマンド
  • 呼び出し元:計算機
  • レシーバー:加算器

まず、アンドゥとリドゥの機能を実装しないことから始めましょう:

public class Test
{
 public static void main(String[] args) 
 {
 Calculator calculator = new Calculator(new AddCommand());
 calculator.add(3);
 calculator.add(9);
 }
}
interface Command
{
 int execute(int value);
}
class Calculator
{
 private Command command;
 public Calculator(Command command)
 {
 this.command = command;
 }
 public void add(int value)
 {
 System.out.println(command.execute(value));
 }
}
class Adder
{
 private int num = 0;
 public int add(int value)
 {
 return num += value;
 }
}
class AddCommand implements Command
{
 private Adder adder = new Adder();
 @Override
 public int execute(int value)
 {
 return adder.add(value);
 }
}

コードは上記の例と同様ですので、説明は省略します。

ここで重要なのは、undoとredoの関数をどのように実装するかということです。 undoは足し算が行われた状態に戻すことができ、redoは足し算が行われた状態に戻すことができます。これは順番が決まっているので、現在の状態を示す添え字、undoを示す添え字の左方向へのシフト、redoを示す添え字の右方向へのシフトを使用して、配列と関連付けることができます:

状態の配列は、実行された各追加の状態を格納するために使用され、添え字は現在の状態を示し、元に戻すときは添え字を左に、やり直すときは添え字を右に移動します。

まず、undoとredoのメソッドを追加するために、抽象コマンドクラスを変更する必要があります:

interface Command
{
 int execute(int value);
 int undo();
 int redo();
}

次に、呼び出し元クラスを修正し、undoメソッドとredoメソッドを追加します:

class Calculator
{
 private Command command;
 public Calculator(Command command)
 {
 this.command = command;
 }
 public void add(int value)
 {
 System.out.println(command.execute(value));
 }
 public void undo()
 {
 System.out.println(command.undo());
 }
 public void redo()
 {
 System.out.println(command.redo());
 }
}

コア実装は、状態を格納するために List を使用するレシーバ・クラスの Adder にあります。Index は添え字を表し、undo または redo で最初に添え字の位置が合法かどうかを判断します:

class Adder
{
 private List<Integer> nums = new ArrayList<>();
 private int index = 0;
 public Adder()
 {
 nums.add(0);
 }
 public int add(int value)
 {
 int result = nums.get(index)+value;
 nums.add(result);
 ++index;
 return result;
 }
 public int redo()
 {
 if(index + 1 < nums.size())
 return nums.get(++index);
 return nums.get(index);
 }
 public int undo()
 {
 if(index - 1 >= 0)
 return nums.get(--index);
 return nums.get(index);
 }
}

最後に、特定のコマンドクラスは、単にアンドゥとリドゥのメソッドを追加します:

class AddCommand implements Command
{
 private Adder adder = new Adder();
 @Override
 public int execute(int value)
 {
 return adder.add(value);
 }
 @Override
 public int undo()
 {
 return adder.undo();
 }
 @Override
 public int redo()
 {
 return adder.redo();
 }
}

テスト:

public static void main(String[] args) 
{
 Calculator calculator = new Calculator(new AddCommand());
 calculator.add(3);
 calculator.add(9);
 
 calculator.undo();
 calculator.undo();
 calculator.undo();
 calculator.undo();
 
 calculator.redo();
 calculator.redo();
 calculator.redo();
 calculator.redo();
}

主な利点

  • 結合の低減: リクエスタとレシーバの間には直接の参照はないので、 リクエスタとレシーバは完全に切り離され、同じリクエストは異なるレシーバ に対応することができ、同じレシーバを異なるリクエスタが使用することができ、 両者は独立性が高い。
  • OCPを満たす:新しいコマンドをシステムに簡単に追加することができ、新しい特定のコマンドクラスを追加しても他のクラスには影響しないため、新しい特定のコマンドクラスを追加することは簡単で、OCPの要件を満たしています。
  • 取り下げ+中間:リクエストの取り下げとやり直しのための設計と実装ソリューションを提供します。

主な欠点

  • 具体的なコマンドクラスが多すぎる: コマンドパターンを使用すると、具体的なコマンドクラスが多すぎるシステムになる可能性があります。具体的なツールクラスは、リクエストレシーバの呼び出し操作ごとに設計する必要があるため、システムによっては多数の具体的なコマンドクラスを提供する必要があります。

シナリオ

  • システムは、リクエストの呼び出し側とリクエストの受け取り側を切り離す必要があり、呼び出し側と受け取り側が直接やり取りすることはありません。
  • システムは異なる時間にリクエストを指定し、リクエストをキューに入れ、実行する必要があります。
  • システムは元に戻す操作と復元操作をサポートする必要があります。
  • システムは、一連の操作を組み合わせてマクロ・コマンドを形成する必要があります。

Read next

タイムスタンプを取得する

日のタイムスタンプを0から取得 1日は86400秒なので、7日前のタイムスタンプは 1日は86400秒なので、7日前のタイムスタンプは js 日付の日の開始時刻と終了時刻を取得します。

Oct 21, 2020 · 1 min read