Modèle de conception pour gérer une réponse

10

La plupart du temps, lorsque j'écris du code qui gère la réponse pour un certain appel de fonction, j'obtiens la structure de code suivante:

exemple: il s'agit d'une fonction qui gérera l'authentification pour un système de connexion

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

Problème

  1. Comme vous pouvez le voir, le code est construit sur une if/elsestructure, ce qui signifie qu'un nouveau statut d'échec signifie que je dois ajouter une else ifinstruction qui constitue une violation du principe ouvert et fermé .
  2. J'ai l'impression que la fonction a différentes couches d'abstraction car je peux simplement augmenter le compteur d'essais de connexion dans un gestionnaire, mais faire des choses plus sérieuses dans un autre.
  3. Certaines fonctions sont répétées increase the login trialspar exemple.

J'ai pensé à convertir le multiple if/elseen un motif d'usine, mais je n'ai utilisé l'usine que pour créer des objets et non pour modifier les comportements. Quelqu'un at-il une meilleure solution pour cela?

Remarque:

Ceci est juste un exemple d'utilisation d'un système de connexion. Je demande une solution générale à ce comportement en utilisant un modèle OO bien construit. Ce type de if/elsegestionnaires apparaît à trop d'endroits dans mon code et je viens d'utiliser le système de connexion comme exemple simple et facile à expliquer. Mes cas d'utilisation réels sont trop compliqués à publier ici. :RÉ

Veuillez ne pas limiter votre réponse au code PHP et n'hésitez pas à utiliser la langue que vous préférez.


MISE À JOUR

Un autre exemple de code plus compliqué juste pour clarifier ma question:

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

but de la fonction:

  • Je vends des jeux sur ebay.
  • Si un client souhaite annuler sa commande et récupérer son argent (c'est-à-dire un remboursement), je dois d'abord ouvrir un "Litige" sur ebay.
  • Une fois qu'un litige est ouvert, je dois attendre que le client confirme qu'il accepte le remboursement (idiot car c'est lui qui m'a dit de rembourser, mais c'est comme ça que ça fonctionne sur ebay).
  • Cette fonction obtient tous les litiges ouverts par moi et vérifie périodiquement leur statut pour voir si le client a répondu au litige ou non.
  • Le client peut accepter (puis je rembourse) ou refuser (puis je recule) ou peut ne pas répondre pendant 7 jours (je clore le litige moi-même puis rembourser).
Songo
la source

Réponses:

15

Il s'agit d'un candidat de choix pour le modèle de stratégie .

Par exemple, ce code:

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
}

Pourrait être réduit à

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

où getOrderStrategy encapsule la commande dans une DisputeAcceptedStrategy, DisputeCancelledStrategy, DisputeOlderThan7DaysStrategy, etc., chacun sachant comment gérer la situation donnée.

Modifier, pour répondre à la question dans les commentaires.

pourriez-vous s'il vous plaît élaborer davantage sur votre code. Ce que j'ai compris, c'est que getOrderStrategy est une méthode d'usine qui renvoie un objet de stratégie en fonction de l'état de la commande, mais quelles sont les fonctions preProcess () et preProcess (). Aussi pourquoi avez-vous passé $ this à updateOrderHistory ($ this)?

Vous vous concentrez sur l'exemple, ce qui pourrait être complètement inapproprié pour votre cas. Je n'ai pas assez de détails pour être sûr de la meilleure implémentation, j'ai donc donné un vague exemple.

Le seul morceau de code que vous avez est insertRecordInOrderHistoryTable, j'ai donc choisi de l'utiliser (avec un nom légèrement plus générique) comme point central de la stratégie. Je lui passe $ this, car il appelle une méthode à ce sujet, avec $ order et une chaîne différente par stratégie.

Donc, fondamentalement, j'imagine chacun de ceux qui ressemblent à ceci:

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

Où $ order est un membre privé de la stratégie (rappelez-vous que j'ai dit qu'il devrait envelopper la commande) et que le deuxième argument est différent dans chaque classe. Encore une fois, cela pourrait être tout à fait inapproprié. Vous voudrez peut-être déplacer insertRecordInOrderHistoryTable vers une classe de stratégie de base et ne pas passer la classe d'autorisation. Ou vous voudrez peut-être faire quelque chose de complètement différent, ce n'est qu'un exemple.

De même, j'ai limité le reste du code différent aux méthodes de pré et post-traitement. Ce n'est certainement pas le meilleur que vous puissiez faire avec. Donnez-lui des noms plus appropriés. Divisez-le en plusieurs méthodes. Tout ce qui rend le code appelant plus lisible.

Vous préférerez peut- être le faire:

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

Et demandez à certaines de vos stratégies d'implémenter des méthodes vides pour les méthodes "IfNecessary".

Tout ce qui rend le code appelant plus lisible.

pdr
la source
Merci pour votre réponse, mais pourriez-vous développer davantage votre code. Ce que j'ai compris, c'est qu'il getOrderStrategys'agit d'une méthode d'usine qui renvoie un strategyobjet en fonction du statut de la commande, mais quelles sont les fonctions preProcess()et preProcess(). Et pourquoi es-tu passé $thisà updateOrderHistory($this)?
Songo
1
@Songo: J'espère que la modification ci-dessus vous aidera.
pdr
Ah! Je pense que je comprends maintenant. Certainement un vote
Songo
+1, pouvez-vous préciser si la ligne, var $ stratégie = $ this.getOrderStrategy ($ order); aurait un boîtier de commutation pour identifier la stratégie.
Naveen Kumar
2

Le modèle de stratégie est une bonne suggestion si vous voulez vraiment décentraliser votre logique, mais cela semble être un excès d'indirection pour des exemples aussi petits que le vôtre. Personnellement, j'emploierais le modèle "écrire des fonctions plus petites", comme:

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

Lorsque vous commencez à avoir un tas d'instructions if / then / else pour gérer un état, considérez le modèle d'état .

Il y avait une question sur une manière particulière de l'utiliser: cette mise en œuvre du modèle d'état a-t-elle un sens?

Je suis nouveau dans ce bagout, mais j'ai quand même mis en place la réponse pour m'assurer de bien comprendre quand l'utiliser (éviter les «tous les problèmes ressemblent à des clous pour un marteau».).

JeffO
la source
0

Comme je l'ai dit dans mes commentaires, la logique complexe ne change vraiment rien.

Vous souhaitez traiter une commande contestée. Il y a plusieurs façons de procéder. Le type de commande contesté peut être 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();
   }
}

Il y a plusieurs façons de procéder. Vous pouvez avoir hiérarchie d'héritage de Order, DisputedOrder, DisputedOrderLessThan7Days, DisputedOrderCanceled, etc. Ce n'est pas beau, mais il fonctionne également.

Dans mon exemple ci-dessus, je regarde le type de commande et j'obtiens une stratégie pertinente pour cela. Vous pouvez encapsuler ce processus dans une usine:

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

Cela examinerait le type de commande et vous donnerait une stratégie correcte pour ce type de commande.

Vous pourriez vous retrouver avec quelque chose comme:

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

Réponse originale, plus pertinente car je pensais que vous vouliez quelque chose de plus simple:

Je vois ici les préoccupations suivantes:

  • Vérifiez l'IP interdite. Vérifie si l'IP de l'utilisateur est dans une plage IP interdite. Vous l'exécuterez quoi qu'il arrive.
  • Vérifiez si les essais ont été dépassés. Vérifie si l'utilisateur a dépassé ses tentatives de connexion. Vous l'exécuterez quoi qu'il arrive.
  • Authentifier l'utilisateur. Essayez d'authentifier l'utilisateur.

Je ferais ce qui suit:

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

Actuellement, votre exemple a trop de responsabilités. Tout ce que j'ai fait, c'était d'encapsuler ces responsabilités dans les méthodes. Le code semble plus propre et vous n'avez pas de déclarations de condition partout.

Factory encapsule la construction d'objets. Vous n'avez pas besoin d'encapsuler la construction de quoi que ce soit dans votre exemple, tout ce que vous avez à faire est de séparer vos préoccupations.

CodeART
la source
Merci pour votre réponse, mais mes gestionnaires pour chaque statut de réponse peuvent être très complexes. veuillez consulter la mise à jour de la question.
Songo
Cela ne change rien. La responsabilité est de traiter la commande contestée en utilisant une certaine stratégie. La stratégie varierait selon le type de différend.
CodeART
Veuillez voir une mise à jour. Pour une logique plus complexe, vous pouvez utiliser l'usine pour construire vos stratégies de commande contestées.
CodeART
1
+1 Merci pour la mise à jour. C'est beaucoup plus clair maintenant.
Songo