応答を処理するためのデザインパターン


10

ほとんどの場合、特定の関数呼び出しの応答を処理するコードを書いているとき、次のコード構造が得られます。

例:これはログインシステムの認証を処理する関数です

class Authentication{

function login(){ //This function is called from my Controller
$result=$this->authenticate($username,$password);

if($result=='wrong password'){
   //increase the login trials counter
   //send mail to admin
   //store visitor ip    
}else if($result=='wrong username'){
   //increase the login trials counter
   //do other stuff
}else if($result=='login trials exceeded')
   //do some stuff
}else if($result=='banned ip'){
   //do some stuff
}else if...

function authenticate($username,$password){
   //authenticate the user locally or remotely and return an error code in case a login in fails.
}    
}

問題

  1. ご覧のとおり、コードはif/else構造に基づいて構築されています。つまり、新しい障害ステータスはelse ifOpen Closed Principleに違反するステートメントを追加する必要があることを意味します。
  2. 1つのハンドラーでログイントライアルカウンターを増やすだけで、別のハンドラーではより深刻な処理を行うため、関数の抽象化レイヤーが異なるように感じます。
  3. increase the login trialsたとえば、一部の機能が繰り返されています。

複数if/elseをファクトリパターンに変換することを考えましたが、動作を変更せずにオブジェクトを作成するためにのみファクトリを使用しました。誰かがこれのためのより良い解決策を持っていますか?

注意:

これはログインシステムを使用した例にすぎません。十分に構築されたOOパターンを使用して、この動作に対する一般的な解決策を求めています。この種のif/elseハンドラーはコードの非常に多くの場所に表示されており、単純に説明しやすい例としてログインシステムを使用しました。私の実際の使用例はここに投稿するのが非常に複雑です。:D

PHPコードへの回答を制限せず、希望する言語を自由に使用してください。


更新

私の質問を明確にするための別のより複雑なコード例:

  public function refundAcceptedDisputes() {            
        $this->getRequestedEbayOrdersFromDB(); //get all disputes requested on ebay
        foreach ($this->orders as $order) { /* $order is a Doctrine Entity */
            try {
                if ($this->isDisputeAccepted($order)) { //returns true if dispute was accepted
                    $order->setStatus('accepted');
                    $order->refund(); //refunds the order on ebay and internally in my system
                    $this->insertRecordInOrderHistoryTable($order,'refunded');                        
                } else if ($this->isDisputeCancelled($order)) { //returns true if dispute was cancelled
                    $order->setStatus('cancelled');
                    $this->insertRecordInOrderHistory($order,'cancelled');
                    $order->rollBackRefund(); //cancels the refund on ebay and internally in my system
                } else if ($this->isDisputeOlderThan7Days($order)) { //returns true if 7 days elapsed since the dispute was opened
                    $order->closeDispute(); //closes the dispute on ebay
                    $this->insertRecordInOrderHistoryTable($order,'refunded');
                    $order->refund(); //refunds the order on ebay and internally in my system
                }
            } catch (Exception $e) {
                $order->setStatus('failed');
                $order->setErrorMessage($e->getMessage());
                $this->addLog();//log error
            }
            $order->setUpdatedAt(time());
            $order->save();
        }
    }

機能目的:

  • 私はebayでゲームを販売しています。
  • 顧客が彼の注文のキャンセルを希望し、彼の返金(つまり、払い戻し)を希望する場合、最初にebayで「紛争」を開かなければなりません。
  • 異議申し立てが開始されたら、お客様が払い戻しに同意することをお客様が確認するのを待つ必要があります(返金するように私に言ったのは愚かですが、それがebayでの動作です)。
  • この機能は、私がすべての紛争を解決し、そのステータスを定期的にチェックして、顧客が紛争に返信したかどうかを確認します。
  • お客様は同意する(払い戻しする)か拒否する(その後ロールバックする)か、7日間応答しない場合があります(異議申し立てを閉じてから払い戻します)。

回答:


15

これは戦略パターンの第一候補です。

たとえば、次のコード:

if ($this->isDisputeAccepted($order)) { //returns true if dispute was accepted
    $order->setStatus('accepted');
    $order->refund(); //refunds the order on ebay and internally in my system
    $this->insertRecordInOrderHistoryTable($order,'refunded');                        
} else if ($this->isDisputeCancelled($order)) { //returns true if dispute was cancelled
    $order->setStatus('cancelled');
    $this->insertRecordInOrderHistory($order,'cancelled');
    $order->rollBackRefund(); //cancels the refund on ebay and internally in my system
} else if ($this->isDisputeOlderThan7Days($order)) { //returns true if 7 days elapsed since the dispute was opened
    $order->closeDispute(); //closes the dispute on ebay
    $this->insertRecordInOrderHistoryTable($order,'refunded');
    $order->refund(); //refunds the order on ebay and internally in my system
}

に減らすことができる

var $strategy = $this.getOrderStrategy($order);
$strategy->preProcess();
$strategy->updateOrderHistory($this);
$strategy->postProcess();

ここで、getOrderStrategyは、DisputeAcceptedStrategy、DisputeCancelledStrategy、DisputeOlderThan7DaysStrategyなどで注文をラップします。それぞれ、特定の状況を処理する方法を知っています。

編集して、コメントで質問に答えます。

コードについてもう少し詳しく説明していただけませんか。私が理解したのは、getOrderStrategyは注文ステータスに応じて戦略オブジェクトを返すファクトリメソッドですが、preProcess()およびpreProcess()関数とは何ですか。また、なぜ$ thisをupdateOrderHistory($ this)に渡したのですか?

あなたはあなたのケースに完全に不適切であるかもしれない例に焦点を合わせています。最良の実装を確実にするのに十分な詳細がないので、漠然とした例を思いつきました。

あなたが持っているコードの一般的な部分の1つはinsertRecordInOrderHistoryTableです。そのため、私はそれを(少し一般的な名前で)戦略の中心点として使用することにしました。$ thisをそれに渡します。これは、$ orderと戦略ごとに異なる文字列を使用して、このメソッドを呼び出すためです。

だから、基本的に、私はそれぞれが次のように見えることを想像します:

public function updateOrderHistory($auth) {
    $auth.insertRecordInOrderHistoryTable($order, 'cancelled');
}

$ orderは戦略のプライベートメンバーであり(注文をラップする必要があることを思い出してください)、2番目の引数はクラスごとに異なります。繰り返しますが、これは完全に不適切な場合があります。insertRecordInOrderHistoryTableを基本のStrategyクラスに移動し、Authorizationクラスを渡さないようにすることもできます。または、まったく異なることを実行することもできますが、これは単なる例です。

同様に、異なるコードの残りをpre-およびpostProcessメソッドに限定しました。これは、ほとんどの場合、それを使用して実行できる最善の方法ではありません。より適切な名前を付けます。複数のメソッドに分割します。呼び出しコードを読みやすくするものは何でも。

あなたはこれをすることを好むかもしれません

var $strategy = $this.getOrderStrategy($order);
$strategy->setStatus();
$strategy->closeDisputeIfNecessary();
$strategy->refundIfNecessary();
$strategy->insertRecordInOrderHistoryTable($this);                        
$strategy->rollBackRefundIfNecessary();

そしてあなたの戦略のいくつかに "IfNecessary"メソッドのための空のメソッドを実装させてください。

呼び出しコードを読みやすくするものは何でも。


お返事ありがとうございます。コードについて詳しく説明していただけますか。私が理解したのは、注文ステータスに応じてオブジェクトgetOrderStrategyを返すファクトリメソッドですstrategyが、preProcess()およびpreProcess()関数は何ですか。また、なぜあなたはパス$thisしましたupdateOrderHistory($this)か?
Songo、

1
@Songo:上記の編集がお役に立てば幸いです。
pdr 2012年

ああ!もうわかったと思います。間違いなく私からの投票:)
Songo

+1、できます、詳しく説明します、行かどうか、var $ strategy = $ this.getOrderStrategy($ order); 戦略を特定するための切り替えケースがあります。
Naveen Kumar

2

ロジックを分散化したい場合、戦略パターンは良い提案ですが、あなたのような小さな例では、間接的なやり過ぎのように思えます。個人的には、次のような「小さい関数を書く」パターンを採用します。

if($result=='wrong password')
   wrongPassword();
else if($result=='wrong username')
   wrongUsername();
else if($result=='login trials exceeded')
   excessiveTries();
else if($result=='banned ip')
   bannedIp();

1

ステータスを処理するためのif / then / elseステートメントの束を用意し始めたら、状態パターンを検討してください。

それを使用する特定の方法についての質問がありました:状態パターンのこの実装は意味がありますか?

私はこのパターンに不慣れですが、いつでもそれを使用するタイミングを理解できるように答えを出します(「すべての問題はハンマーに釘のように見える」を避けます)。


0

コメントで述べたように、複雑なロジックは実際には何も変更しません。

異議のある注文を処理したい。それには複数の方法があります。異議のある注文タイプは次のEnumとおりです。

public void ProcessDisputedOrder(DisputedOrder order)
{
   switch (order.Type)
   {
       case DisputedOrderType.Canceled:
          var strategy = new StrategyForDisputedCanceledOrder();
          strategy.Process(order);  
          break;

       case DisputedOrderType.LessThan7Days:
          var strategy = new DifferentStrategy();
          strategy.Process(order);
          break;

       default: 
          throw new NotImplementedException();
   }
}

これを行うには多くの方法があります。あなたはの継承階層持つことができOrderDisputedOrderDisputedOrderLessThan7DaysDisputedOrderCanceledこれはいいではないですが、それは仕事もだろう、など。

上記の私の例では、注文タイプを見て、それに関連する戦略を取得します。そのプロセスをファクトリーにカプセル化できます。

var strategy = DisputedOrderStrategyFactory.Instance.Build(order.Type);

これにより、注文タイプが確認され、そのタイプの注文に対する正しい戦略が得られます。

あなたは次のようなものになるかもしれません:

public void ProcessDisputedOrder(DisputedOrder order)
{
   var strategy = DisputedOrderStrategyFactory.Instance.Build(order.Type);   
   strategy.Process(order);
}

元の答え、あなたがもっと簡単なものを求めていたと思ったので、もはや関連性はありません:

ここで次の懸念事項が見られます。

  • 禁止されているIPを確認します。ユーザーのIPが禁止IP範囲にあるかどうかを確認します。何があってもこれを実行します。
  • 試用を超えたかどうかを確認します。ユーザーがログイン試行回数を超えたかどうかを確認します。何があってもこれを実行します。
  • ユーザーを認証します。ユーザーの認証を試みます。

私は次のようにします:

CheckBannedIP(login.IP);
CheckLoginTrial(login);

Authenticate(login.Username, login.Password);

public void CheckBannedIP(string ip)
{
    // If banned then re-direct, else do nothing.
}

public void CheckLoginTrial(LoginAttempt login)
{
    // If exceeded trials, then inform user, else do nothing
}

public void Authenticate(string username, string password)
{
     // Attempt to authenticate. On success redirect, else catch any errors and inform the user. 
}

現在、あなたの例には責任が多すぎます。私がしたのは、それらの責任をメソッド内にカプセル化することだけでした。コードはすっきりしているように見え、至る所に条件ステートメントがありません。

ファクトリはオブジェクトの構築をカプセル化します。あなたはあなたの例で何かの構成をカプセル化する必要はありません、あなたがする必要があるのはあなたの懸念を分離することだけです。


お返事ありがとうございます。ただし、各応答ステータスのハンドラは非常に複雑になる場合があります。質問の更新をご覧ください。
Songo

何も変わりません。責任は、いくつかの戦略を使用して係争中の注文を処理することです。戦略は紛争の種類によって異なります。
CodeART、2012年

アップデートをご覧ください。より複雑なロジックについては、問題のある注文戦略を構築するためにファクトリを利用できます。
CodeART、2012年

1
+1更新ありがとうございます。今ではもっとはっきりしています。
Songo
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.