ほとんどの場合、特定の関数呼び出しの応答を処理するコードを書いているとき、次のコード構造が得られます。
例:これはログインシステムの認証を処理する関数です
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.
}
}
問題
if/else
構造に基づいて構築されています。つまり、新しい障害ステータスは、Openの違反であるelse if
ステートメントを追加する必要があることを意味します閉じた原理。increase the login trials
で繰り返されます。複数のif/else
をファクトリパターンに変換することを考えましたが、動作を変更せずにオブジェクトを作成するためにのみファクトリを使用しました。誰かがこれのためのより良い解決策を持っていますか?
注:
これはログインシステムを使用した単なる例です。十分に構築されたOOパターンを使用したこの動作の一般的な解決策を求めています。この種類のif/else
ハンドラーがコード内のあまりにも多くの場所に表示され、ログインシステムを単純で簡単に説明できる例として使用しました。ここでの投稿はrealユースケースが非常に複雑です。 :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();
}
}
関数の目的:
これは 戦略パターン の最有力候補です。
たとえば、次のコード:
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メソッドに限定しました。これは、ほとんどの場合、それを使用して実行できる最善の方法ではありません。より適切な名前を付けます。複数のメソッドに分割します。呼び出しコードを読みやすくするものは何でも。
あなたmightはこれを好む:
var $strategy = $this.getOrderStrategy($order);
$strategy->setStatus();
$strategy->closeDisputeIfNecessary();
$strategy->refundIfNecessary();
$strategy->insertRecordInOrderHistoryTable($this);
$strategy->rollBackRefundIfNecessary();
そしてあなたの戦略のいくつかに "IfNecessary"メソッドのための空のメソッドを実装させてください。
呼び出しコードを読みやすくするものは何でも
ロジックを分散化したい場合、戦略パターンは良い提案ですが、あなたのような小さな例では、間接的なやり過ぎのように見えます。個人的には、次のような「小さい関数を書く」パターンを採用します。
if($result=='wrong password')
wrongPassword();
else if($result=='wrong username')
wrongUsername();
else if($result=='login trials exceeded')
excessiveTries();
else if($result=='banned ip')
bannedIp();
ステータスを処理するためのif/then/elseステートメントの束を使い始めたら、 状態パターン を検討してください。
それを使用する特定の方法についての質問がありました: 状態パターンのこの実装は意味がありますか?
私はこのパターンに不慣れですが、いつでもそれを使用するタイミングを理解できるように答えを出します(「すべての問題はハンマーに釘のように見える」を避けます)。
コメントで述べたように、複雑なロジックは実際には何も変更しません。
異議のある注文を処理したい。それには複数の方法があります。異議のある注文タイプは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();
}
}
これを行うには多くの方法があります。 Order
、DisputedOrder
、DisputedOrderLessThan7Days
、DisputedOrderCanceled
などの継承階層を持つことができます。これはナイスではありませんが、機能します。
上記の私の例では、注文タイプを見て、それに関連する戦略を取得します。そのプロセスをファクトリーにカプセル化できます。
var strategy = DisputedOrderStrategyFactory.Instance.Build(order.Type);
これにより、注文タイプが確認され、そのタイプの注文に対する正しい戦略が得られます。
あなたは次のようなもので終わるかもしれません:
public void ProcessDisputedOrder(DisputedOrder order)
{
var strategy = DisputedOrderStrategyFactory.Instance.Build(order.Type);
strategy.Process(order);
}
元の答え、あなたがもっと簡単なものを求めていたと思っていたので、もはや関係ありません:
ここに次の懸念事項があります。
私は次のようにします:
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.
}
現在、あなたの例には責任が多すぎます。私がしたのは、それらの責任をメソッド内にカプセル化することだけでした。コードがすっきりしているように見え、至る所に条件ステートメントがありません。
ファクトリはオブジェクトの構築をカプセル化します。あなたの例では何かの構成をカプセル化する必要はありません、あなたがする必要があるのはあなたの懸念を分離することだけです。