38 votes

Le polymorphisme ou les conditionnels favorisent-ils une meilleure conception?

Je suis récemment tombé sur cette entrée dans le google blog de tests sur les lignes directrices pour écrire plus de code de tests. J'ai été d'accord avec l'auteur de jusqu'à ce point:

Faveur polymorphisme sur le conditionnel: Si vous voyez une instruction switch, vous devriez penser à des polymorphismes. Si vous voyez la même si la condition répété dans de nombreux endroits dans votre classe que vous devez penser de nouveau le polymorphisme. Polymorphisme va briser votre classe complexe en plusieurs petits plus simple, les classes qui définissent clairement les morceaux de code sont liées et exécuter ensemble. Cela permet de tester depuis plus simple/plus petite est la classe la plus facile à tester.

J'ai simplement ne peut pas envelopper la tête autour de cela. Je peux comprendre l'utilisation du polymorphisme au lieu de RTTI (ou de BRICOLAGE-RTTI, selon le cas peut être), mais il semble qu'une telle déclaration générale que je ne peux pas imaginer ce fait être utilisé efficacement dans le code de production. Il me semble, plutôt, qu'il serait plus facile d'ajouter des cas de test pour les méthodes qui ont les instructions switch, plutôt que de casser le code dans des dizaines de catégories distinctes.

Aussi, j'ai eu l'impression que le polymorphisme peut conduire à toutes sortes d'autres bogues et des problèmes de conception, de sorte que je suis curieux de savoir si le compromis ici en vaudrait la peine. Quelqu'un peut m'expliquer exactement ce que l'on entend par cette directive d'essai?

72voto

Loki Astari Points 116129

En fait, ce qui rend les tests et le code est plus facile à écrire.

Si vous avez une instruction switch basé sur un champ interne vous avez probablement le même commutateur dans de multiples lieux de faire des choses légèrement différentes. Cela provoque des problèmes lorsque vous ajoutez un nouveau cas que vous devez mettre à jour tous les instructions de commutation (si vous pouvez les trouver).

En utilisant le polymorphisme vous pouvez utiliser des fonctions virtuelles pour obtenir la même fonctionnalité et parce qu'un nouveau cas est une nouvelle classe, vous n'avez pas à rechercher votre code pour des choses qui ont besoin d'être vérifiée, il est tout isolé pour chaque classe.

class Animal
{
    public:
       Noise warningNoise();
       Noise pleasureNoise();
    private:
       AnimalType type;
};

Noise Animal::warningNoise()
{
    switch(type)
    {
        case Cat: return Hiss;
        case Dog: return Bark;
    }
}
Noise Animal::pleasureNoise()
{
    switch(type)
    {
        case Cat: return Purr;
        case Dog: return Bark;
    }
}

Dans ce cas simple, chaque nouvel animal provoque exige à la fois des instructions de commutation à être mis à jour.
Vous ne vous rappelez? Quel est le défaut? BANG!!

L'utilisation du polymorphisme

class Animal
{
    public:
       virtual Noise warningNoise() = 0;
       virtual Noise pleasureNoise() = 0;
};

class Cat: public Animal
{
   // Compiler forces you to define both method.
   // Otherwise you can't have a Cat object

   // All code local to the cat belongs to the cat.

};

En utilisant le polymorphisme vous pouvez tester l'Animal de la classe.
Ensuite, l'essai de chacune des classes dérivées séparément.

Aussi ce qui vous permet d'expédier l'Animal de la classe (Fermé pour l'altération) comme faisant partie de vous binaire de la bibliothèque. Mais les gens peuvent toujours ajouter de nouveaux Animaux (Ouvert pour l'extension) par dérivation de nouvelles classes dérivées de l'Animal d'en-tête. Si toutes ces fonctionnalités avaient été capturés à l'intérieur de l'Animal de la classe, alors tous les animaux doivent être définis avant l'expédition (fermé/Fermé).

26voto

paercebal Points 38526

Ne craignez pas...

Je suppose que ton problème est lié à la familiarité, pas la technologie. Familiarisez-vous avec le C++ programmation orientée objet.

C++ est un langage de programmation orientée objet

Parmi ses multiples paradigmes, il a de la POO fonctionnalités et est plus que capable de soutenir la comparaison avec la plupart pur langage OO.

Ne laissez pas le "C partie à l'intérieur de C++" vous faire croire que C++ ne peut pas traiter avec d'autres paradigmes. C++ peut gérer beaucoup de paradigmes de programmation tout à fait gracieusement. Et parmi eux, la programmation orientée objet C++ est le plus mature de C++ paradigmes après la procédure de paradigme (c'est à dire le fameux "C").

Le polymorphisme est Ok pour la production

Il n'y a pas de "bogues" ou "ne convient pas pour la production de code" de la chose. Il y a des développeurs qui restent ancrés dans leurs habitudes, et les développeurs qui allez apprendre à utiliser les outils et l'utilisation des meilleurs outils pour chaque tâche.

l'interrupteur et le polymorphisme sont presque similaire...

... Mais polymorphisme supprimé la plupart des erreurs.

La différence est que vous devez gérer les commutateurs manuellement, alors que le polymorphisme est plus naturel, une fois que vous avez utilisé avec de l'héritage de la redéfinition de méthode.

Avec les commutateurs, vous aurez pour comparer une variable de type avec différents types, et de gérer les différences. Avec le polymorphisme, la variable elle-même ne sait comment se comporter. Vous n'avez qu'à organiser les variables dans la logique des moyens, et de remplacer les bonnes méthodes.

Mais en fin de compte, si vous oubliez de s'occuper d'une affaire en switch, le compilateur ne vous dis pas, alors que l'on vous dira si vous dériver d'une classe sans écraser ses méthodes virtuelles pures. Ainsi, la plupart des switch-les erreurs sont évitées.

Dans l'ensemble, les deux caractéristiques sont sur les choix à faire. Mais Polymorphisme de vous permettre de faire plus complexe et, dans le même temps, de plus naturel et donc plus de choix.

Évitez d'utiliser des RTTI à trouver un type d'objet

RTTI est un concept intéressant, et peut être utile. Mais la plupart du temps (c'est à dire 95% du temps), la redéfinition de méthode et l'héritage sera plus que suffisant, et la plupart de votre code doit même pas connaître le type exact de l'objet manipulé, mais la confiance, à faire la bonne chose.

Si vous utilisez RTTI comme une simple interrupteur, vous êtes à côté de la question.

(Disclaimer: je suis un grand fan de la RTTI concept et de dynamic_casts. Mais on doit utiliser le bon outil pour la tâche à accomplir, et la plupart du temps de RTTI est utilisé comme une simple interrupteur, ce qui est faux)

Comparer dynamique vs statique polymorphisme

Si votre code ne connais pas le type exact d'un objet au moment de la compilation, puis utilisez polymorphisme dynamique (c'est à dire classique héritage, méthodes virtuelles primordial, etc.)

Si votre code ne connaît la nature au moment de la compilation, alors peut-être que vous pourriez utiliser polymorphisme statique, c'est à dire le modèle PFI http://en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern

Le PROGRAMME vous permettra d'avoir un code qui sent le polymorphisme dynamique, mais dont chaque appel de méthode sera résolu statiquement, ce qui est idéal pour certains très critique de code.

La Production de code exemple

Un code similaire à celui-ci (de mémoire) est utilisé sur la production.

La plus simple solution de révolution autour de la procédure appelée par la boucle de message (un WinProc dans Win32, mais j'ai écrit une version plus simple, pour des raisons de simplicité). Afin de résumer, c'était quelque chose comme:

void MyProcedure(int p_iCommand, void *p_vParam)
{
   // A LOT OF CODE ???
   // each case has a lot of code, with both similarities
   // and differences, and of course, casting p_vParam
   // into something, depending on hoping no one
   // did a mistake, associating the wrong command with
   // the wrong data type in p_vParam

   switch(p_iCommand)
   {
      case COMMAND_AAA: { /* A LOT OF CODE (see above) */ } break ;
      case COMMAND_BBB: { /* A LOT OF CODE (see above) */ } break ;
      // etc.
      case COMMAND_XXX: { /* A LOT OF CODE (see above) */ } break ;
      case COMMAND_ZZZ: { /* A LOT OF CODE (see above) */ } break ;
      default: { /* call default procedure */} break ;
   }
}

Chaque ajout de la commande ajout d'un cas.

Le problème est que certaines commandes similaires, et partagé en partie, par leur mise en œuvre.

Donc, en mélangeant le cas d'un risque pour l'évolution.

J'ai résolu le problème en utilisant le modèle de Commande, qui est, la création d'une base de l'objet de Commande, avec une méthode process ().

J'ai donc ré-écrit le message de la procédure, en minimisant le code dangereux (c'est à dire jouer avec void *, etc.) à un minimum, et il a écrit pour être sûr que je n'aurais jamais besoin de le toucher à nouveau:

void MyProcedure(int p_iCommand, void *p_vParam)
{
   switch(p_iCommand)
   {
      // Only one case. Isn't it cool?
      case COMMAND:
         {
           Command * c = static_cast<Command *>(p_vParam) ;
           c->process() ;
         }
         break ;
      default: { /* call default procedure */} break ;
   }
}

Puis, pour chaque commande, au lieu d'ajouter du code dans la procédure, et le mélange (ou pire, de le copier/coller) le code à partir des commandes similaires, j'ai créé une nouvelle commande, et la dérivée à partir de l'objet de Commande, ou l'un de ses objets dérivés:

Cela a conduit à la hiérarchie (représentés par un arbre):

[+] Command
 |
 +--[+] CommandServer
 |   |
 |   +--[+] CommandServerInitialize
 |   |
 |   +--[+] CommandServerInsert
 |   |
 |   +--[+] CommandServerUpdate
 |   |
 |   +--[+] CommandServerDelete
 |
 +--[+] CommandAction
 |   |
 |   +--[+] CommandActionStart
 |   |
 |   +--[+] CommandActionPause
 |   |
 |   +--[+] CommandActionEnd
 |
 +--[+] CommandMessage

Maintenant, tout ce que j'avais à faire était de remplacer les processus pour chaque objet.

Simple et facile à étendre.

Par exemple, dire que le CommandAction était censé faire son processus en trois phases: "avant", "pendant" et "après". Son code serait quelque chose comme:

class CommandAction : public Command
{
   // etc.
   virtual void process() // overriding Command::process pure virtual method
   {
      this->processBefore() ;
      this->processWhile() ;
      this->processAfter() ;
   }

   virtual void processBefore() = 0 ; // To be overriden

   virtual void processWhile()
   {
      // Do something common for all CommandAction objects
   }

   virtual void processAfter()  = 0 ; // To be overriden

} ;

Et, par exemple, CommandActionStart pourrait être codé sous la forme:

class CommandActionStart : public CommandAction
{
   // etc.
   virtual void processBefore()
   {
      // Do something common for all CommandActionStart objects
   }

   virtual void processAfter()
   {
      // Do something common for all CommandActionStart objects
   }
} ;

Comme je l'ai dit: Facile à comprendre (si un commentaire est correctement), et très facile à étendre.

Le commutateur est réduite à son strict minimum (c'est à dire si-comme, parce que nous avons encore besoin de déléguer des commandes de Windows à Windows par défaut de la procédure), et pas besoin de RTTI (ou pire, dans la maison de RTTI).

Le même code à l'intérieur d'un switch serait assez amusant, je suppose (si ce n'est à en juger par la quantité de "historiques" du code, j'ai vu dans notre application au travail).

10voto

Vincent Ramdhanie Points 46265

Unité de test d'une OO programme à tester chaque classe comme une unité. Un principe que vous voulez apprendre, c'est "Ouvert à l'extension, fermée à la modification". J'ai reçu que de la Tête la Première, les Modèles de Conception. Mais il dit essentiellement que vous souhaitez avoir la possibilité d'étendre facilement votre code sans modification code à tester.

Polymorphisme rend cela possible en éliminant les instructions conditionnelles. Considérons cet exemple:

Supposons que vous avez un objet Personnage qui porte une Arme. Vous pouvez écrire une méthode d'attaque comme ceci:

If (weapon is a rifle) then //Code to attack with rifle else
If (weapon is a plasma gun) //Then code to attack with plasma gun

etc.

Avec le polymorphisme le Personnage n'a pas de "connaître" le type d'arme, tout simplement

weapon.attack()

serait de travailler. Ce qui se passe si une nouvelle arme a été inventé? Sans polymorphisme, vous devrez modifier votre instruction conditionnelle. Avec le polymorphisme, vous devrez ajouter une nouvelle classe et de laisser le Caractère testé classe seul.

8voto

rice Points 739

Je suis un peu sceptique: je crois que l'héritage ajoute souvent plus de complexité qu'il supprime.

Je pense que vous êtes de poser une bonne question, cependant, et une chose que je considère est ceci:

Êtes-vous diviser en plusieurs classes, parce que vous avez affaire avec différentes choses? Ou est-ce vraiment la même chose, agissant dans un autre chemin?

Si c'est vraiment un nouveau type, puis aller de l'avant et de créer une nouvelle classe. Mais si c'est juste une option, en général, je le garder dans la même classe.

Je crois que la solution par défaut est la seule classe, et c'est au programmeur de proposer l'héritage de prouver leur cas.

5voto

JohnMcG Points 5062

N'étant pas un expert dans les implications pour les cas de test, mais à partir d'un logiciel point de vue du développement:

  • Ouvert-fermé principe -- Classes doivent être fermées à l'altération, mais ouvert à d'extension. Si vous gérez conditionnelles opérations par l'intermédiaire d'une expression conditionnelle, alors si une nouvelle condition est ajoutée, votre classe doit changer. Si vous utiliser le polymorphisme, la classe de base ne doivent pas changer.

  • Ne vous répétez pas -- Une partie importante de la ligne directrice est la "même si la condition." Qui indique que votre classe a certains modes de fonctionnement distincts qui peuvent être pris en compte dans une classe. Ensuite, cette condition s'affiche dans un endroit dans votre code, lorsque vous créez une instance de l'objet pour ce mode. Et encore une fois, si un nouveau arrive, il vous suffit de modifier un bout de code.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X