メインコンテンツへ移動

デザインパターン:ステートパターン(State Pattern)

無料2015-03-07#Design_Pattern#状态模式#State Pattern

ステートパターンはある状態下の一連の動作をカプセル化するために使用されます。ステートパターンは状態切り替えプロセスを隠蔽し、呼び出し者はモジュール内部の状態遷移の詳細を認識しません。ステートパターンはプログラム実行時のポリモーフィズムを実現します。コード中に大量の類似した if-else 構造が現れた場合、ステートパターンを使用してこれらの不調和な条件ブロックを消除する必要があるかもしれません。

no_mkd

一.ステートパターンとは何か?

すべてのアクションをステートオブジェクトにカプセル化し、ステートホルダーは現在のステートオブジェクトに動作を委譲します

つまり、ステートホルダー(例えば自動車、テレビ、ATM 機は複数の状態を持ちます)は動作の詳細を知らず、ステートホルダーは自分が現在置かれている状態(どのステートオブジェクトを保持しているか)のみを気にし、すべてのことを現在のステートオブジェクトに任せればよいのです。もちろん、ステートホルダーには状態切り替えを制御する権利がありますし、選択して何もしないこともできます。。)

二.例を挙げる

ATM 機をシミュレートすると仮定し、以下の要件があります:

  • 現金引き出し、カードパスワード認証、現金吐出、サービス終了
  • カードパスワード認証失敗または残高不足の場合、カードを返し、今回のサービスを終了する
  • 機械内に現金がない場合、No Cash を表示し、銀行に現金不足情報を送信する
  • 機械故障の場合、No Service を表示し、保守担当者に保守リクエストを送信する

ユーザーの現金引き出しプロセスは以下のようになります:

これはユーザー操作のフローです。ATM 機にとっては以下の点にも注意する必要があります:

  • ユーザー入力金額を取得した後、ユーザーのカード内残高だけでなく、ATM 機内の残高も検証する必要がある
  • 毎回の成功した現金引き出しサービス終了後、ATM 機内の残高をチェックする必要がある(現金がない場合は対応処理を行う)
  • あらゆるリンクで処理できないエラーが発生した場合、故障として処理する

上記の現金引き出しプロセスを考えると、さまざまな不正操作も考慮する必要があります。例えば:

  • カードを挿入せずに直接パスワードを入力する
  • 機械故障時にも引き続き操作を行う
  • 機械内に現金がない時に現金引き出しを要求する

細分化した後、詳細な問題が非常に面倒になることがわかります。機械はそこに置かれており、すべてのユーザーインターフェースが開放されています。ユーザーはいつでも任意のインターフェースを使用する可能性があります。これらの不正操作を防ぐために、一連の判断を追加せざるを得ません。例えば:

  • パスワードを検証する前に、カードが挿入されているか、および機械が故障していないかチェックする必要がある
  • カードを挿入する前に、機械が故障していないかチェックする必要がある
  • 現金引き出し操作の前に、機械が故障していないか、現金がないか、是否。。。

コード中には機械の状態を示すために大量のメンバー変数を設定する必要があり、さらに恐ろしいのはロジックブロック内に大量の if-else が存在し、しかも各操作内の if-else ブロックにはわずかな違いしかないことです。冗長に見えるが、処理しにくい(各判断を独立したメソッドとして抽出しても、コード規模を縮小できますが、処理プロセス上は依然として冗長です。。)

より良い処理方法はステートパターンを使用することで、状態判断部分の冗長性を完全に消除し、明確で整頓されたコード構造を提供できます。以下でステートパターンを使用して例中の要件を実装します

(1)まず ATM が提供するすべてのインターフェースを見つける

  1. カード挿入
  2. パスワード送信
  3. 現金引き出し(現金引き出しボタンは物理キーと仮定)
  4. 照会(同上と仮定)
  5. カード取り出し

(2)次に ATM のすべての状態および各状態に対応する動作を見つける

  • 準備完了(Ready)、利用可能インターフェース:すべて
  • 現金不足(NoCash)、利用可能インターフェース:1、2、4、5
  • 故障(NoService)、利用可能インターフェース:なし

(3)コーディング実装

まず State ベースクラスを定義し、クラス内に(1)でリストしたすべてのインターフェースをカプセル化します:

package StatePattern;

/**

  • 定義 ATM 機状態

  • @author ayqy / public interface ATMState { /*

    • 插卡 */ public abstract void insertCard();

    /**

    • 提交密码 */ public abstract void submitPwd();

    /**

    • 取款 */ public abstract void getCash();

    /**

    • 查询余额 */ public abstract void queryBalance();

    /**

    • 取卡 */ public abstract void ejectCard(); }

次に 3 つの状態を逐一実装します

ReadyState:

package StatePattern;

/**

  • 実装 ATM 準備状態

  • @author ayqy */ public class ReadyState implements ATMState{ private ATM atm;//状態ホルダーの参照を保持し、操作できるようにする

    public ReadyState(ATM atm){ this.atm = atm; }

    @Override public void insertCard() { System.out.println("カード挿入完了"); }

    @Override public void submitPwd() { System.out.println("パスワード送信完了"); //パスワードを検証し対応処理を行う if("123".equals(atm.getPwd())){ System.out.println("パスワード認証通過"); } else{ System.out.println("パスワード認証失��"); //カードを返す ejectCard(); } }

    @Override public void getCash() { if(atm.getTotalAmount() >= atm.getAmount() && atm.getBalance() >= atm.getAmount()){ //口座残高を更新 atm.setBalance(atm.getBalance() - atm.getAmount()); //機械内現金総額を更新 atm.setTotalAmount(atm.getTotalAmount() - atm.getAmount()); System.out.println("¥" + atm.getAmount() + "吐出"); System.out.println("現金引き出し完了"); //カードを返す ejectCard(); //機械内残钞をチェック if(atm.getTotalAmount() == 0){//現金がない場合、NoService 状態に切り替え atm.setCurrState(atm.getNoCashState()); System.out.println("現金不足情報を銀行に送信しました"); } } else{ System.out.println("現金引き出し失敗、残高不足"); //カードを返す ejectCard(); } }

    @Override public void queryBalance() { System.out.println("残高¥" + atm.getBalance()); System.out.println("残高照会完了"); }

    @Override public void ejectCard() { System.out.println("カード取り出し完了"); } }

ステートクラス内で状態切り替えを行う部分に注意してください:

if(atm.getTotalAmount() == 0){//現金がない場合、NoService 状態に切り替え
	atm.setCurrState(atm.getNoCashState());
}

直接具体的なステートオブジェクトを new するのではなく、ATM が提供する set インターフェースを使用しています。こうするのは可能な限り結合を解くためです(兄弟オブジェクトはお互いを認識していません)。より多くの柔軟性を獲得するためです

NoCashState を実装:

package StatePattern;

/**

  • 実装 ATM 現金不足状態

  • @author ayqy */ public class NoCashState implements ATMState{ private ATM atm;//状態ホルダーの参照を保持し、操作できるようにする

    public NoCashState(ATM atm){ this.atm = atm; }

    @Override public void insertCard() { System.out.println("カード挿入完了"); }

    @Override public void submitPwd() { System.out.println("パスワード送信完了"); //パスワードを検証し対応処理を行う if("123".equals(atm.getPwd())){ System.out.println("パスワード認証通過"); } else{ System.out.println("パスワード認証失敗"); //カードを返す ejectCard(); } }

    @Override public void getCash() { System.out.println("現金引き出し失敗、機械内に現金なし"); }

    @Override public void queryBalance() { System.out.println("残高¥" + atm.getBalance()); System.out.println("残高照会完了"); }

    @Override public void ejectCard() { System.out.println("カード取り出し完了"); } }

NoServiceState を実装:

package StatePattern;

/**

  • 実装 ATM 故障状態

  • @author ayqy */ public class NoServiceState implements ATMState{ private ATM atm;//状態ホルダーの参照を保持し、操作できるようにする

    public NoServiceState(ATM atm){ this.atm = atm; }

    @Override public void insertCard() { System.out.println("カード挿入失敗、機械が故障しました"); }

    @Override public void submitPwd() { System.out.println("パスワード送信失敗、機械が故障しました"); }

    @Override public void getCash() { System.out.println("現金引き出し失敗、機械が故障しました"); }

    @Override public void queryBalance() { System.out.println("残高照会失敗、機械が故障しました"); }

    @Override public void ejectCard() { System.out.println("カード取り出し失敗、機械が故障しました"); } }

具体的なステートを実装したので、ATM クラスを構築できます。以下のようになります:

package StatePattern;

/**

  • 実装 ATM 機

  • @author ayqy */ public class ATM { /すべての状態/ private ATMState readyState; private ATMState noCashState; private ATMState noServiceState;

    private ATMState currState;//現在の状態 private int totalAmount;//機械内現金総額

    /テスト用の一時変数/ private String pwd;//パスワード private int balance;//残高 private int amount;//引き出し金額

    public ATM(int totalAmount, int balance, int amount, String pwd) throws Exception{ //すべての状態を初期化 readyState = new ReadyState(this); noCashState = new NoCashState(this); noServiceState = new NoServiceState(this);

     if(totalAmount > 0){
     	currState = readyState;
     }
     else if(totalAmount == 0){
     	currState = noCashState;
     }
     else{
     	throw new Exception();
     }
     
     //テストデータを初期化
     this.totalAmount = totalAmount;
     this.balance = balance;
     this.amount = amount;
     this.pwd = pwd;
    

    }

    /具体的な動作をステートオブジェクトに委譲/ /**

    • カード挿入 */ public void insertCard(){ currState.insertCard(); }

    /**

    • パスワード送信 */ public void submitPwd(){ currState.submitPwd(); }

    /**

    • 現金引き出し */ public void getCash(){ currState.getCash(); }

    /**

    • 残高照会 */ public void queryBalance(){ currState.queryBalance(); }

    /**

    • カード取り出し */ public void ejectCard(){ currState.ejectCard(); }

    public String toString(){ return "現金総額¥" + totalAmount; }

    /ここに大量の getter and setter を省略/ }

すべて完了したので、早速テストしてみましょう

三.実行例

まず Test クラスを実装します:

package StatePattern;

import java.util.Scanner;

/**

  • 実装テストクラス

  • @author ayqy / public class Test { public static void main(String[] args) { /テストデータ/ / 機械内総額 口座残高 引き出し金額 パスワード * 1000 500 200 123 * 1000 300 500 123 * 0 500 200 123 * */ try { test(1000, 500, 200, "123"); System.out.println("-------"); test(1000, 300, 500, "123"); System.out.println("-------"); test(0, 500, 200, "123"); } catch (Exception e) { System.out.println("機械故障、保守リクエストを保守先に送信しました"); } }

    private static void test(int totalAmount, int balance, int amount, String pwd)throws Exception{ //ATM を作成 ATM atm; atm = new ATM(totalAmount, balance, amount, pwd); //初期状態を出力 System.out.println(atm.toString()); atm.insertCard(); atm.submitPwd(); atm.getCash(); //終了状態を出力 System.out.println(atm.toString()); } }

設計した 3 つのユースケース(正常な現金引き出し、残高を超える引き出し、機械内に現金なし)の実行結果は以下の通りです:

四.ステートパターンとストラテジーパターン

ストラテジーパターンを覚えていますか?これら 2 つが非常に似ているとは思いませんか?

その通りです。这两种パターンのクラス図は完全に同一です。説明します:

  • ステート主体(所有者)はステートオブジェクトを保持し、実行時にステートオブジェクトを動的に指定することでクラスの動作を変更できる
  • ストラテジー主体はアルゴリズムファミリーオブジェクトを保持し、実行時にアルゴリズムファミリー内のアルゴリズム(ストラテジー)を動的に選択することでクラスの動作を変更できる

つまり、ステートパターンとストラテジーパターンはどちらも実行時のポリモーフィズムをサポートし、その実装方法はどちらもコンポジション + デリゲーションです。しかし、これら 2 つのパターンが同一であることを意味するわけではありません。なぜなら、それらの目標が異なるからです:

  • ステートパターンはアルゴリズムフローの変更可能を実現する(つまり状態切り替え。異なる状態には異なるフローがある)
  • ストラテジーパターンはアルゴリズム詳細の選択可能を実現する(つまりアルゴリズムファミリー内のアルゴリズムを選択。1 つのアルゴリズムファミリーは複数の選択可能アルゴリズムを含む)

コメント

コメントはまだありません

コメントを書く