Avantages de plusieurs méthodes sur Switch

12

J'ai reçu un examen du code d'un développeur senior aujourd'hui demandant "Au fait, quelle est votre objection à la répartition des fonctions par le biais d'une instruction switch?" J'ai lu à de nombreux endroits sur le fait que pomper un argument via des méthodes de basculement vers un appel est une mauvaise POO, pas aussi extensible, etc. Cependant, je ne peux pas vraiment trouver de réponse définitive pour lui. Je voudrais régler cela une fois pour toutes.

Voici nos suggestions de codes concurrents (php utilisé comme exemple, mais peut s'appliquer de manière plus universelle):

class Switch {
   public function go($arg) {
      switch ($arg) {
         case "one":
            echo "one\n";
         break;
         case "two":
            echo "two\n";
         break;
         case "three":
            echo "three\n";
         break;
         default:
            throw new Exception("Unknown call: $arg");
         break;
      }
   }
}

class Oop {
   public function go_one() {
      echo "one\n";
   }
   public function go_two() {
      echo "two\n";
   }
   public function go_three() {
      echo "three\n";
   }
   public function __call($_, $__) {
      throw new Exception("Unknown call $_ with arguments: " . print_r($__, true));
   }
}

Une partie de son argument était "Il (méthode switch) a une manière beaucoup plus propre de gérer les cas par défaut que ce que vous avez dans la méthode magique générique __call ()."

Je ne suis pas d'accord sur la propreté et préfère en fait appeler, mais j'aimerais entendre ce que les autres ont à dire.

Arguments que je peux trouver à l'appui du Oopschéma:

  • Un peu plus propre en termes de code que vous devez écrire (moins, plus facile à lire, moins de mots clés à considérer)
  • Toutes les actions ne sont pas déléguées à une seule méthode. Pas beaucoup de différence d'exécution ici, mais au moins le texte est plus compartimenté.
  • Dans le même esprit, une autre méthode peut être ajoutée n'importe où dans la classe au lieu d'un emplacement spécifique.
  • Les méthodes sont à espace de noms, ce qui est bien.
  • Ne s'applique pas ici, mais considérons un cas où Switch::go()opéré sur un membre plutôt qu'un paramètre. Vous devez d'abord changer le membre, puis appeler la méthode. Car Oopvous pouvez appeler les méthodes indépendamment à tout moment.

Arguments que je peux trouver à l'appui du Switchschéma:

  • Pour des raisons d'argument, une méthode plus propre pour traiter une demande par défaut (inconnue)
  • Semble moins magique, ce qui pourrait rendre les développeurs peu familiers plus à l'aise

Quelqu'un a-t-il quelque chose à ajouter de chaque côté? J'aimerais avoir une bonne réponse pour lui.

Pilules d'explosion
la source
@Justin Satyr J'y ai pensé, mais je pense que cette question concerne plus spécifiquement le code et la recherche d'une solution optimale et convient donc au stackoverflow. Et comme le dit @ yes123, plus de gens sont susceptibles de répondre ici.
__call est mauvais. Il détruit complètement les performances et vous pouvez l'utiliser pour appeler une méthode qui est censée être privée aux appelants externes.
Ooppermet d'avoir phpdoc pour décrire chaque méthode, qui peut être analysée par certains IDE (par exemple, NetBeans).
binaryLV
fluffycat.com/PHP-Design-Patterns/… .. Apparemment, Switch n'est pas si efficace. -1
@GordonM: Et si la classe en question n'a pas de méthodes privées?
JAB

Réponses:

10

Un commutateur n'est pas considéré comme une POO car souvent le polymorphisme peut faire l'affaire.

Dans votre cas, une implémentation POO pourrait être la suivante:

class Oop 
{
  protected $goer;

  public function __construct($goer)
  {
    $this->goer = $goer;
  }

  public function go()
  {
    return $this->goer->go();
  }
}

class Goer
{
  public function go()
  {
    //...
  }
}

class GoerA extends Goer
{
  public function go()
  {
    //...
  }
}

class GoerB extends Goer
{
  public function go()
  {
    //...
  }
}

class GoerC extends Goer
{
  public function go()
  {
    //...
  }
}


$oop = new Oop(new GoerB());
$oop->go();
Ando
la source
1
Le polymorphisme est la bonne réponse. +1
Rein Henrichs
2
C'est bien, mais l'inconvénient est une prolifération excessive de code. Vous devez avoir une classe pour chaque méthode et c'est plus de code à gérer .. et plus de mémoire. N'y a-t-il pas un juste milieu?
Explosion Pills
8

pour cet exemple:

class Switch
{
    public function go($arg)
    {
        echo "$arg\n";
    }
}

OK, je ne plaisante que partiellement ici. L'argument pour / contre l'utilisation d'une instruction switch ne peut pas être fortement avancé avec un exemple aussi trivial, car le côté POO dépend de la sémantique impliquée, et pas seulement du mécanisme de répartition .

Les instructions switch sont souvent une indication de classes ou de classifications manquantes, mais pas nécessairement. Parfois, une instruction switch n'est qu'une instruction switch .

Steven A. Lowe
la source
+1 pour "la sémantique, pas les mécanismes"
Javier
1

Peut-être pas une réponse, mais dans le cas du code non-switch, il semble que ce serait une meilleure correspondance:

class Oop {
  /**
   * User calls $oop->go('one') then this function will determine if the class has a 
   * method 'go_one' and call that. If it doesn't, then you get your error.
   * 
   * Subclasses of Oop can either overwrite the existing methods or add new ones.
   */
  public function go($arg){

    if(is_callable(array($this, 'go_'. $arg))){
      return call_user_func(array($this, 'go_'. $arg));
    }

    throw new Exception("Unknown call: $arg");
  }

  public function go_one() {
    echo "one\n";
  }
  public function go_two() {
    echo "two\n";
  }
  public function go_three() {
    echo "three\n";
  }
}

Une grande partie du puzzle à évaluer est ce qui se passe lorsque vous devez créer NewSwitchou NewOop. Vos programmeurs doivent-ils sauter à travers des cerceaux d'une manière ou d'une autre? Que se passe-t-il lorsque vos règles changent, etc.

Rob
la source
J'aime votre réponse - j'allais publier la même chose, mais vous avez été ninja. Je pense que vous devriez changer method_exists en is_callable () pour éviter les problèmes avec les méthodes protégées héritées et vous pouvez changer call_user_func en $ this -> {'go _'. $ Arg} () pour rendre votre code plus lisible. Une autre chose - maby ajoute un commentaire expliquant pourquoi la méthode magique __call est mauvaise - elle ruine la fonctionnalité is_callable () sur cet objet ou cette instance car elle retournera toujours TRUE.
J'ai mis à jour is_callable, mais j'ai laissé call_user_func car (ne faisant pas partie de cette question), il peut y avoir d'autres arguments à transmettre. Mais maintenant que cela est passé aux programmeurs, je suis définitivement d'accord pour dire que la réponse de @ Andrea est bien meilleure :)
Rob
0

Au lieu de simplement mettre votre code d'exécution en fonctions, implémentez un modèle de commande complet et placez chacun d'eux dans leur propre classe qui implémente une interface commune. Cette méthode vous permet d'utiliser IOC / DI pour câbler les différents «cas» de votre classe et vous permet d'ajouter et de supprimer facilement des cas de votre code au fil du temps. Il vous donne également un bon bout de code qui ne viole pas les principes de programmation SOLID.

P. Roe
la source
0

Je pense que l'exemple est mauvais. S'il y a une fonction go () qui accepte l'argument $ where, if est parfaitement valide pour utiliser switch dans la fonction. La simple fonction go () facilite la modification du comportement de tous les go_where () scénarious. De plus, vous préserverez l'interface de classe - si vous utilisez différentes méthodes, vous modifiez l'interface de la classe avec chaque nouvelle destination.

En fait, switch ne doit pas être remplacé par un ensemble de méthodes, mais par un ensemble de classes - c'est du polymorphisme. Ensuite, la destination sera gérée par chaque sous-classe, et il y aura une seule méthode go () pour toutes. Remplacer le conditionnel par le polymorphisme est l'un des refactorings de base, décrit par Martin Fowler. Mais vous n'avez peut-être pas besoin de polymorphisme, et le changement est la voie à suivre.

Maxim Krizhanovsky
la source
Si vous ne changez pas l'interface et qu'une sous-classe a sa propre implémentation, go()elle peut facilement violer le principe de substitution de Liskov. Si vous ajoutez plus de fonctionnalités go(), vous ne voulez changer l'interface pour autant que je suis concerend.
Explosion Pills