응답 처리를위한 디자인 패턴


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구조를 기반으로 빌드 됨을 의미합니다. 즉, 새로운 실패 상태는 Open Closed Principle에else if 위반 되는 문장 을 추가해야 함을 의미합니다 .
  2. 한 처리기에서 로그인 시도 카운터를 증가시킬 수 있지만 다른 처리기에서 더 심각한 작업을 수행 할 수 있으므로 함수에 다른 추상화 계층이 있다는 느낌이 들었습니다.
  3. 일부 기능은 increase the login trials예를 들어 반복 됩니다.

다중 if/else을 팩토리 패턴으로 변환하는 것에 대해 생각 했지만 동작을 변경하지 않는 객체를 만들기 위해 팩토리 만 사용했습니다. 누구든지 이것에 대한 더 나은 해결책이 있습니까?

노트 :

이것은 로그인 시스템을 사용하는 예일뿐입니다. 잘 구축 된 OO 패턴을 사용 하여이 동작에 대한 일반적인 솔루션을 요청하고 있습니다. 이런 종류의 if/else핸들러는 내 코드의 너무 많은 곳에 나타나며 로그인 시스템을 간단한 설명하기 쉬운 예제로 사용했습니다. 내 실제 사용 사례는 여기에 게시하기가 너무 복잡합니다. :디

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에서 "분쟁"을 열어야합니다.
  • 분쟁이 시작되면 고객이 환불에 동의한다는 것을 확인할 때까지 기다려야합니다 (환불하라고 말한 사람이 아니라 이베이에서 작동하는 방식 임).
  • 이 기능은 본인이 공개 한 모든 분쟁을 가져오고 고객이 분쟁에 응답했는지 여부를 정기적으로 확인합니다.
  • 고객이 동의 (환불)하거나 거부 (환불)하거나 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)에 전달 했습니까?

귀하의 사례에 완전히 부적합 할 수있는 예에 초점을 맞추고 있습니다. 최상의 구현을 보장하기에 충분한 세부 정보가 없으므로 모호한 예를 생각해 냈습니다.

가장 일반적인 코드 조각은 insertRecordInOrderHistoryTable이므로 전략의 중심점으로 약간 더 일반적인 이름을 사용하기로 선택했습니다. $ order와 전략마다 다른 문자열을 사용하여 메소드를 호출하기 때문에 $ this를 전달합니다.

따라서 기본적으로 다음과 같은 각 사람을 구상합니다.

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

$ order가 Strategy의 비공개 멤버이고 (주문을 래핑해야한다고 말한 것을 기억하십시오) 두 번째 인수는 각 클래스마다 다릅니다. 다시 말하지만 이것은 전적으로 부적절 할 수 있습니다. 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

아하! 나는 지금 그것을 얻는다 생각한다. 나에게서 확실히 투표 :)
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 문이 시작되면 State Pattern을 고려하십시오 .

그것을 사용하는 특별한 방법에 대한 질문이있었습니다. 이 상태 패턴의 구현이 의미가 있습니까?

나는이 패턴을 처음 접했지만 어쨌든 그것을 언제 사용해야하는지 이해하기 위해 답을 썼다 ( "모든 문제는 망치에 못처럼 보인다"는 것을 피하라).


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();
   }
}

이를 수행하는 방법에는 여러 가지가 있습니다. 당신의 상속 계층 구조를 가질 수있다 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);
}

원래 답변, 더 간단한 것으로 생각한 것처럼 더 이상 관련이 없습니다.

여기에 다음과 같은 문제가 있습니다.

  • 금지 된 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

업데이트를 참조하십시오. 좀 더 복잡한 논리를 위해 분쟁이 발생한 주문 전략을 세우기 위해 공장을 활용할 수 있습니다.
CodeART

1
+1 업데이트 해 주셔서 감사합니다. 지금은 훨씬 더 명확합니다.
Songo
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.