Design pattern per la gestione di una risposta

10

La maggior parte delle volte in cui scrivo del codice che gestisce la risposta per una determinata chiamata di funzione ottengo la seguente struttura di codice:

esempio: questa è una funzione che gestirà l'autenticazione per un sistema di login

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.
}    
}

problema

  1. Come puoi vedere il codice è basato su una struttura if/else il che significa che un nuovo stato di errore significherà che devo aggiungere un'istruzione else if che è una violazione per il Open Closed Principle .
  2. Ho la sensazione che la funzione abbia diversi livelli di astrazione in quanto potrei semplicemente aumentare il contatore delle prove di accesso in un gestore, ma fare cose più serie in un altro.
  3. Alcune funzioni sono ripetute increase the login trials ad esempio.

Ho pensato di convertire il multiplo if/else in uno schema di fabbrica, ma ho usato solo la fabbrica per creare oggetti che non alteravano i comportamenti. Qualcuno ha una soluzione migliore per questo?

Nota:

Questo è solo un esempio utilizzando un sistema di accesso. Sto chiedendo una soluzione generale a questo comportamento usando un modello OO ben costruito. Questo tipo di if/else handler appare in troppi punti nel mio codice e ho appena usato il sistema di login come un semplice esempio di facile spiegazione. I miei casi d'uso reali sono molto più complicati da pubblicare qui. : D

Non limitare la tua risposta al codice PHP e sentiti libero di usare la lingua che preferisci.

Aggiorna

Un altro esempio di codice più complicato solo per chiarire la mia domanda:

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

scopo della funzione:

  • Sto vendendo giochi su ebay.
  • Se un cliente desidera annullare il suo ordine e ottiene indietro i suoi soldi (Ad esempio un rimborso) Devo prima aprire una "contestazione" su ebay.
  • Una volta aperta una controversia, devo aspettare che il cliente confermi ciò accetta il rimborso (sciocco perché è lui che mi ha detto di rimborsare, ma è così che funziona su ebay).
  • Questa funzione ottiene tutte le controversie aperte da me e controlla periodicamente i loro stati per vedere se il cliente ha risposto o meno alla controversia.
  • Il cliente può concordare (quindi rimborsare) o rifiutare (quindi eseguo il rollback) o non rispondere per 7 giorni (chiudo la contestazione quindi rimborserò).
posta Songo 01.05.2012 - 18:53
fonte

4 risposte

14

Questo è un candidato ideale per il modello di strategia .

Ad esempio, questo codice:

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
}

Potrebbe essere ridotto a

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

dove getOrderStrategy esegue il wrapping dell'ordine in una DisputeAcceptedStrategy, DisputeCancelledStrategy, DisputeOlderThan7DaysStrategy, ecc. ognuno dei quali sa come gestire la situazione data.

Modifica, per rispondere alla domanda nei commenti.

could you please elaborate more on your code. What I understood is that getOrderStrategy is a factory method that returns a strategy object depending on the order status, but what are the preProcess() and preProcess() functions. Also why did you pass $this to updateOrderHistory($this)?

Ti stai concentrando sull'esempio, che potrebbe essere completamente inappropriato per il tuo caso. Non ho abbastanza dettagli per essere sicuro della migliore implementazione, quindi ho trovato un esempio vago.

L'unico codice comune che hai è insertRecordInOrderHistoryTable, quindi ho scelto di usarlo (con un nome leggermente più generico) come punto centrale della strategia. Ho passato $ a questo, perché sta chiamando un metodo su questo, con ordine $ e una stringa diversa per strategia.

Quindi, in sostanza, immagino che ognuno di questi abbia questo aspetto:

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

Dove $ order è un membro privato della strategia (ricorda che ho detto che dovrebbe avvolgere l'ordine) e il secondo argomento è diverso in ogni classe. Ancora una volta, questo potrebbe essere del tutto inappropriato. Potresti voler spostare insertRecordInOrderHistoryTable in una classe Strategy di base e non passare la classe Authorization. O potresti voler fare qualcosa di completamente diverso, era solo un esempio.

Allo stesso modo, ho limitato il resto del codice diverso ai metodi pre- e postProcess. Questo è quasi certamente il meglio che puoi fare con esso. Dagli nomi più appropriati. Suddividilo in più modi. Qualunque cosa renda il codice chiamante più leggibile.

potresti preferire questo:

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

E alcune delle tue strategie implementano metodi vuoti per i metodi "IfNecessari".

Qualunque cosa rende il codice chiamante più leggibile.

    
risposta data 01.05.2012 - 20:05
fonte
3

Il modello di strategia è un buon suggerimento se vuoi veramente decentralizzare la tua logica, ma sembra un eccesso eccessivo di indiretti per esempi piccoli come i tuoi. Personalmente, utilizzerei il modello "scrivi funzioni più piccole", come:

if($result=='wrong password')
   wrongPassword();
else if($result=='wrong username')
   wrongUsername();
else if($result=='login trials exceeded')
   excessiveTries();
else if($result=='banned ip')
   bannedIp();
    
risposta data 01.05.2012 - 21:21
fonte
1

Quando inizi a disporre di una serie di istruzioni if / then / else per gestire uno stato, considera il Pattern di stato .

C'era una domanda su un particolare modo di usarlo: Questa implementazione dello schema di stato ha senso?

Sono nuovo di questo patter, ma ho comunque dato la risposta per essere sicuro di capire quando usarlo (Evita "tutti i problemi sembrano chiodi su un martello.").

    
risposta data 01.05.2012 - 20:00
fonte
0

Come ho detto nei miei commenti, la logica complessa in realtà non cambia nulla.

Vuoi elaborare un ordine contestato. Ci sono molti modi per farlo. Il tipo di ordine contestato può essere 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();
   }
}

Ci sono molti modi per farlo. Puoi avere una gerarchia di ereditarietà di Order , DisputedOrder , DisputedOrderLessThan7Days , DisputedOrderCanceled , ecc. Questo non è bello, ma funzionerebbe anche.

Nel mio esempio sopra, guardo il tipo di ordine e ottengo una strategia pertinente per questo. Potresti incapsulare quel processo in una fabbrica:

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

Verrebbe esaminato il tipo di ordine e ti daremo una strategia corretta per quel tipo di ordine.

Potresti finire con qualcosa sulle linee di:

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

Risposta originale, non più pertinente poiché pensavo che fossi alla ricerca di qualcosa di più semplice:

Vedo le seguenti preoccupazioni qui:

  • Verifica l'IP vietato. Controlla se l'IP dell'utente si trova in un intervallo IP vietato. Lo eseguirai indipendentemente da cosa.
  • Verifica se le prove hanno superato. Controlla se l'utente ha superato i propri tentativi di accesso. Lo eseguirai indipendentemente da cosa.
  • Autentica utente. Tenta di autenticare l'utente.

Farei quanto segue:

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. 
}

Attualmente il tuo esempio ha troppe responsabilità. Tutto ciò che ho fatto, è stato incapsulare quelle responsabilità all'interno dei metodi. Il codice sembra più pulito e non si hanno dichiarazioni condizionali dappertutto.

La fabbrica incapsula la costruzione di oggetti. Non è necessario incapsulare la costruzione di qualcosa nel tuo esempio, tutto ciò che devi fare è separare le tue preoccupazioni.

    
risposta data 01.05.2012 - 19:22
fonte

Leggi altre domande sui tag