Les énumérations créent-elles des interfaces fragiles?

17

Considérez l'exemple ci-dessous. Toute modification de l'énumération ColorChoice affecte toutes les sous-classes IWindowColor.

Les énumérations ont-elles tendance à provoquer des interfaces fragiles? Existe-t-il quelque chose de mieux qu'une énumération pour permettre une plus grande flexibilité polymorphe?

enum class ColorChoice
{
    Blue = 0,
    Red = 1
};

class IWindowColor
{
public:
    ColorChoice getColor() const=0;
    void setColor( const ColorChoice value )=0;
};

Edit: désolé d'utiliser la couleur comme exemple, ce n'est pas de cela qu'il s'agit. Voici un exemple différent qui évite le hareng rouge et fournit plus d'informations sur ce que j'entends par flexibilité.

enum class CharacterType
{
    orc = 0,
    elf = 1
};

class ISomethingThatNeedsToKnowWhatTypeOfCharacter
{
public:
    CharacterType getCharacterType() const;
    void setCharacterType( const CharacterType value );
};

De plus, imaginez que les poignées de la sous-classe ISomethingThatNeedsToKnowWhatTypeOfCharacter appropriée soient distribuées par un modèle de conception d'usine. J'ai maintenant une API qui ne peut pas être étendue à l'avenir pour une autre application où les types de caractères autorisés sont {humain, nain}.

Edit: Juste pour être plus concret sur ce sur quoi je travaille. Je conçois une liaison forte de cette spécification ( MusicXML ) et j'utilise des classes enum pour représenter les types de la spécification qui sont déclarés avec xs: enumeration. J'essaie de penser à ce qui se passera lorsque la prochaine version (4.0) sortira. Ma bibliothèque de classes peut-elle fonctionner en mode 3.0 et en mode 4.0? Si la prochaine version est 100% rétrocompatible, alors peut-être. Mais si les valeurs d'énumération ont été supprimées de la spécification, je suis mort dans l'eau.

Matthew James Briggs
la source
2
Quand vous dites "flexibilité polymorphe", quelles capacités avez-vous en tête exactement?
Ixrec
3
L'utilisation d'une énumération pour les couleurs crée des interfaces fragiles, pas seulement «l'utilisation d'une énumération».
Doc Brown
3
L'ajout d'une nouvelle variante d'énumération rompt le code à l'aide de cette énumération. L'ajout d'une nouvelle opération à une énumération est en revanche assez autonome, car tous les cas qui doivent être traités sont là (comparez cela avec les interfaces et les superclasses, où l'ajout d'une méthode non par défaut est un changement majeur). Cela dépend du type de changements qui sont vraiment nécessaires.
1
Concernant MusicXML: S'il n'y a pas de moyen facile de savoir à partir des fichiers XML quelle version du schéma chacun utilise, cela me semble être un défaut de conception critique dans la spécification. Si vous devez le contourner d'une manière ou d'une autre, il n'y a probablement aucun moyen pour nous d'aider jusqu'à ce que nous sachions exactement ce qu'ils choisiront de casser en 4.0 et que vous puissiez poser une question sur un problème spécifique qu'il provoque.
Ixrec

Réponses:

25

Lorsqu'ils sont utilisés correctement, les énumérations sont beaucoup plus lisibles et robustes que les «nombres magiques» qu'ils remplacent. Je ne les vois pas normalement rendre le code plus cassant. Par exemple:

  • setColor () n'a pas à perdre de temps à vérifier s'il values'agit d'une valeur de couleur valide ou non. Le compilateur l'a déjà fait.
  • Vous pouvez écrire setColor (Color :: Red) au lieu de setColor (0). Je pense que la enum classfonctionnalité du C ++ moderne vous permet même de forcer les gens à toujours écrire le premier au lieu du second.
  • Généralement peu important, mais la plupart des énumérations peuvent être implémentées avec n'importe quel type intégral de taille, de sorte que le compilateur peut choisir la taille la plus pratique sans vous forcer à penser à de telles choses.

cependant, utilisation d'une énumération pour la couleur est discutable car dans de nombreuses situations (la plupart?), Il n'y a aucune raison de limiter l'utilisateur à un si petit ensemble de couleurs; vous pourriez tout aussi bien les laisser passer des valeurs RVB arbitraires. Sur les projets avec lesquels je travaille, une petite liste de couleurs comme celle-ci n'apparaîtrait jamais que dans le cadre d'un ensemble de «thèmes» ou de «styles» censés agir comme une fine abstraction sur les couleurs du béton.

Je ne suis pas sûr de savoir ce que signifie votre question sur la «flexibilité polymorphe». Les énumérations n'ont pas de code exécutable, il n'y a donc rien à rendre polymorphe. Vous recherchez peut-être le modèle de commande ?

Edit: Après l'édition, je ne sais toujours pas quel type d'extensibilité vous recherchez, mais je pense toujours que le modèle de commande est la chose la plus proche que vous obtiendrez à une "énumération polymorphe".

Ixrec
la source
Où puis-je en savoir plus sur le fait de ne pas laisser 0 passer comme une énumération?
TankorSmash
5
@TankorSmash C ++ 11 a introduit la "classe d'énumération", également appelée "énumérations de portée", qui ne peut pas être implicitement convertie en son type numérique sous-jacent. Ils évitent également de polluer l'espace de noms comme le faisaient les anciens types "enum" de style C.
Matthew James Briggs
2
Les énumérations sont généralement accompagnées d'entiers. Il y a beaucoup de choses étranges qui peuvent se produire dans la sérialisation / désérialisation ou la conversion entre des nombres entiers et des énumérations. Il n'est pas toujours sûr de supposer qu'une énumération aura toujours une valeur valide.
Eric
1
Tu as raison Eric (et c'est un problème que j'ai rencontré plusieurs fois moi-même). Cependant, vous n'avez qu'à vous soucier d'une valeur éventuellement invalide au stade de la désérialisation. Toutes les autres fois que vous utilisez des énumérations, vous pouvez supposer que la valeur est valide (au moins pour les énumérations dans des langues comme Scala - certaines langues n'ont pas de type très fort vérifiant les énumérations).
Kat
1
@Ixrec "car dans de nombreuses situations (la plupart?), Il n'y a aucune raison de limiter l'utilisateur à un si petit ensemble de couleurs" Il existe des cas légitimes. La console .Net émule la console Windows à l'ancienne, qui ne peut avoir du texte que dans une des 16 couleurs (les 16 couleurs de la norme CGA) msdn.microsoft.com/en-us/library/…
Pharap
15

Toute modification de l'énumération ColorChoice affecte toutes les sous-classes IWindowColor.

Non, non. Il y a deux cas: les exécutants seront soit

  • stocker, renvoyer et transmettre des valeurs d'énumération sans jamais les utiliser, auquel cas elles ne sont pas affectées par les modifications de l'énumération, ou

  • opérer sur des valeurs d'énumération individuelles, auquel cas tout changement dans l'énumération doit bien sûr, naturellement, inévitablement, nécessairement , être pris en compte avec un changement correspondant dans la logique du réalisateur.

Si vous mettez "Sphère", "Rectangle" et "Pyramide" dans une énumération "Forme", et passez une telle énumération à une drawSolid()fonction que vous avez écrite pour dessiner le solide correspondant, puis un matin, vous décidez d'ajouter un " Ikositetrahedron "à l'énumération, vous ne pouvez pas vous attendredrawSolid() fonction reste inchangée; Si vous vous attendiez à ce qu'il dessine en quelque sorte des icositetrahedrons sans que vous ayez d'abord à écrire le code réel pour dessiner des icositetrahedrons, c'est votre faute, pas la faute de l'énumération. Donc:

Les énumérations ont-elles tendance à provoquer des interfaces fragiles?

Non, ils ne le font pas. Ce qui cause des interfaces fragiles, c'est que les programmeurs se considèrent comme des ninjas, essayant de compiler leur code sans activer suffisamment d'avertissements. Ensuite, le compilateur ne les avertit pas que leur drawSolid()fonction contient une switchinstruction à laquelle il manque une caseclause pour la valeur d'énumération "Ikositetrahedron" nouvellement ajoutée.

La façon dont il est censé fonctionner est analogue à l'ajout d'une nouvelle méthode virtuelle pure à une classe de base: vous devez ensuite implémenter cette méthode sur chaque héritier, sinon le projet ne construit pas et ne devrait pas construire.


Maintenant, à vrai dire, les énumérations ne sont pas une construction orientée objet. Ils sont davantage un compromis pragmatique entre le paradigme orienté objet et le paradigme de programmation structurée.

La façon pure et simple de faire les objets n'est pas du tout d'avoir des énumérations, mais plutôt d'avoir des objets tout le long.

Donc, la manière purement orientée objet d'implémenter l'exemple avec les solides est bien sûr d'utiliser le polymorphisme: au lieu d'avoir une seule méthode centralisée monstrueuse qui sait tout dessiner et d'avoir à dire quel solide dessiner, vous déclarez un " Classe "solide" avec une draw()méthode abstraite (virtuelle pure) , puis vous ajoutez les sous-classes "Sphère", "Rectangle" et "Pyramide", chacune ayant sa propre implémentation draw()qui sait se dessiner.

De cette façon, lorsque vous introduisez la sous-classe "Ikositetrahedron", vous n'aurez qu'à lui fournir une draw()fonction, et le compilateur vous rappellera de le faire, en ne vous laissant pas instancier "Icositetrahedron" autrement.

Mike Nakis
la source
Des conseils pour lancer un avertissement de temps de compilation pour ces commutateurs? Je lance généralement des exceptions d'exécution; mais le temps de compilation pourrait être génial! Pas tout à fait aussi génial .. mais les tests unitaires me viennent à l'esprit.
Vaughan Hilts
1
Cela fait un moment que je n'ai pas utilisé C ++ pour la dernière fois, donc je ne suis pas sûr, mais je m'attends à ce que la fourniture du paramètre -Wall au compilateur et l'omission de la default:clause le fassent. Une recherche rapide sur le sujet n'a pas donné de résultats plus précis, donc cela pourrait convenir comme sujet d'une autre programmers SEquestion.
Mike Nakis
En C ++, il peut y avoir une vérification de la compilation si vous l'activez. En C #, toute vérification devrait être exécutée. Chaque fois que je prends une énumération sans indicateur comme paramètre, je m'assure de la valider avec Enum.IsDefined, mais cela signifie toujours que vous devez mettre à jour toutes les utilisations de l'énumération manuellement lorsque vous ajoutez une nouvelle valeur. Voir: stackoverflow.com/questions/12531132/…
Mike prend en charge Monica
1
J'espère qu'un programmeur orienté objet ne fera jamais son objet de transfert de données, comme en Pyramidfait savoir comment faire draw()une pyramide. Au mieux, il peut dériver Solidet avoir une GetTriangles()méthode et vous pouvez la transmettre à un SolidDrawerservice. Je pensais que nous nous éloignions des exemples d'objets physiques en tant qu'exemples d'objets dans la POO.
Scott Whitlock
1
C'est ok, je n'ai pas aimé que quelqu'un dénigrait une bonne vieille programmation orientée objet. :) D'autant plus que la plupart d'entre nous combinent la programmation fonctionnelle et la POO plus que strictement la POO.
Scott Whitlock
14

Les énumérations ne créent pas d'interfaces fragiles. Abuser des énumérations le fait.

À quoi servent les énumérations?

Les énumérations sont conçues pour être utilisées comme des ensembles de constantes nommées de manière significative. Ils doivent être utilisés lorsque:

  • Vous savez qu'aucune valeur ne sera supprimée.
  • (Et) Vous savez qu'il est très peu probable qu'une nouvelle valeur soit nécessaire.
  • (Ou) Vous acceptez qu'une nouvelle valeur soit nécessaire, mais rarement suffisante pour justifier la correction de tout le code qui casse à cause de cela.

Bonnes utilisations des énumérations:

  • Jours de la semaine: (selon .Net'sSystem.DayOfWeek ) À moins que vous n'ayez affaire à un calendrier incroyablement obscur, il n'y aura jamais que 7 jours de la semaine.
  • Couleurs non extensibles: (selon .Net System.ConsoleColor) Certains peuvent être en désaccord avec cela, mais .Net a choisi de le faire pour une raison. Dans le système de console .Net, il n'y a que 16 couleurs disponibles pour une utilisation par la console. Ces 16 couleurs correspondent à la palette de couleurs héritées connue sous le nom de CGA ou «Color Graphics Adapter» . Aucune nouvelle valeur ne sera jamais ajoutée, ce qui signifie qu'il s'agit en fait d'une application raisonnable d'une énumération.
  • Énumérations représentant des états fixes: (selon Java Thread.State) Les concepteurs de Java ont décidé que dans le modèle de thread Java, il n'y aura jamais qu'un ensemble fixe d'états dans lesquels a Threadpeut être, et donc pour simplifier les choses, ces différents états sont représentés comme des énumérations . Cela signifie que de nombreuses vérifications basées sur l'état sont de simples if et commutateurs qui, en pratique, fonctionnent sur des valeurs entières, sans que le programmeur n'ait à se soucier de ce que sont réellement les valeurs.
  • Bitflags représentant des options non mutuellement exclusives: (selon .Net System.Text.RegularExpressions.RegexOptions) Les Bitflags sont une utilisation très courante des énumérations. Si commun en fait, que dans .Net, toutes les énumérations ont une HasFlag(Enum flag)méthode intégrée. Elles prennent également en charge les opérateurs au niveau du bit et il y a un FlagsAttributepour marquer une énumération comme étant destinée à être utilisée comme un ensemble d'indicateurs de bit. En utilisant une énumération comme un ensemble d'indicateurs, vous pouvez représenter un groupe de valeurs booléennes dans une seule valeur, ainsi que les indicateurs clairement nommés pour plus de commodité. Cela serait extrêmement bénéfique pour représenter les drapeaux d'un registre d'état dans un émulateur ou pour représenter les autorisations d'un fichier (lecture, écriture, exécution), ou à peu près n'importe quelle situation où un ensemble d'options connexes ne s'excluent pas mutuellement.

Mauvaises utilisations des énumérations:

  • Classes / types de personnages dans un jeu: à moins que le jeu ne soit une démo à un coup que vous n'utiliserez probablement pas à nouveau, les énumérations ne doivent pas être utilisées pour les classes de personnages, car vous voudrez probablement ajouter beaucoup plus de classes. Il est préférable d'avoir une seule classe représentant le personnage et d'avoir un "type" de personnage dans le jeu représenté autrement. Une façon de gérer cela est le modèle TypeObject , d'autres solutions incluent l'enregistrement des types de caractères avec un dictionnaire / registre de types, ou un mélange des deux.
  • Couleurs extensibles: si vous utilisez une énumération pour les couleurs qui peuvent être ajoutées ultérieurement, ce n'est pas une bonne idée de représenter cela sous forme d'énumération, sinon vous serez laissé pour toujours ajouter des couleurs. Ceci est similaire au problème ci-dessus, donc une solution similaire devrait être utilisée (c'est-à-dire une variation de TypeObject).
  • État extensible: si vous avez une machine à états qui peut finir par introduire de nombreux autres états, ce n'est pas une bonne idée de représenter ces états via des énumérations. La méthode préférée consiste à définir une interface pour l'état de la machine, à fournir une implémentation ou une classe qui enveloppe l'interface et délègue ses appels de méthode (semblable au modèle de stratégie ), puis modifie son état en changeant l'implémentation fournie actuellement active.
  • Bitflags représentant des options mutuellement exclusives: si vous utilisez des énumérations pour représenter des drapeaux et que deux de ces drapeaux ne devraient jamais se produire ensemble, vous vous êtes tiré une balle dans le pied. Tout ce qui est programmé pour réagir à l'un des drapeaux réagira soudainement à celui auquel il est programmé pour répondre en premier - ou pire, il pourrait répondre aux deux. Ce genre de situation demande juste des ennuis. La meilleure approche consiste à traiter si possible l'absence de drapeau comme condition alternative (c'est-à-dire que l'absence de Truedrapeau implique False). Ce comportement peut être davantage pris en charge grâce à l'utilisation de fonctions spécialisées (ie IsTrue(flags)et IsFalse(flags)).
Pharap
la source
J'ajouterai des exemples de bitflag si quelqu'un peut trouver un exemple fonctionnel ou bien connu d'énumérations utilisées comme bitflags. Je sais qu'ils existent mais malheureusement je ne m'en souviens pas pour le moment.
Pharap
1
RegexOptions Msdn.microsoft.com/en-us/library/…
BLSully
@BLSully Excellent exemple. Je ne peux pas dire que je les ai jamais utilisés, mais ils existent pour une raison.
Pharap
4

Les énumérations sont une grande amélioration par rapport aux numéros d'identification magiques pour les ensembles fermés de valeurs qui n'ont pas beaucoup de fonctionnalités associées. Habituellement, vous ne vous souciez pas du nombre réellement associé à l'énumération; dans ce cas, il est facile d'étendre en ajoutant de nouvelles entrées à la fin, aucune fragilité ne devrait en résulter.

Le problème est lorsque vous avez des fonctionnalités importantes associées à l'énumération. Autrement dit, vous avez ce genre de code qui traîne:

switch (my_enum) {
case orc: growl(); break;
case elf: sing(); break;
...
}

Un ou deux d'entre eux est correct, mais dès que vous avez ce genre d'énumération significative, ces switchdéclarations ont tendance à se multiplier comme des tribus. Maintenant, chaque fois que vous étendez l'énumération, vous devez rechercher tous les switches associés pour vous assurer que vous avez tout couvert. C'est fragile. Des sources telles que Clean Code suggèrent que vous devriez en avoir un switchpar énumération, tout au plus.

Dans ce cas, vous devez plutôt utiliser les principes OO et créer une interface pour ce type. Vous pouvez toujours conserver l'énumération pour communiquer ce type, mais dès que vous devez en faire quoi que ce soit, vous créez un objet associé à l'énumération, éventuellement à l'aide d'une fabrique. C'est beaucoup plus facile à maintenir, car il vous suffit de chercher un seul endroit à mettre à jour: votre usine, et en ajoutant une nouvelle classe.

congusbongus
la source
1

Si vous faites attention à la façon dont vous les utilisez, je ne considérerais pas les énumérations comme nuisibles. Mais il y a quelques points à considérer si vous souhaitez les utiliser dans du code de bibliothèque, par opposition à une seule application.

  1. Ne jamais supprimer ou réorganiser les valeurs. Si vous avez à un moment donné une valeur enum dans votre liste, cette valeur doit être associée à ce nom pour toute l'éternité. Si vous le souhaitez, vous pouvez à un moment donné renommer des valeurs en deprecated_orcou autre chose, mais en ne les supprimant pas, vous pouvez plus facilement maintenir la compatibilité avec l'ancien code compilé avec l'ancien ensemble d'énumérations. Si un nouveau code ne peut pas traiter les anciennes constantes d'énumération, générez-y une erreur appropriée ou assurez-vous qu'aucune valeur n'atteigne ce morceau de code.

  2. Ne faites pas d'arithmétique sur eux. En particulier, ne faites pas de comparaisons d'ordres. Pourquoi? Parce qu'alors, vous pourriez rencontrer une situation où vous ne pouvez pas conserver les valeurs existantes et conserver un ordre sain en même temps. Prenez par exemple une énumération pour les directions de la boussole: N = 0, NE = 1, E = 2, SE = 3,… Maintenant, si après une mise à jour, vous incluez NNE et ainsi de suite entre les directions existantes, vous pouvez soit les ajouter à la fin de la liste et ainsi casser l'ordre, ou vous les entrelacer avec les clés existantes et ainsi casser les mappages établis utilisés dans le code hérité. Ou vous dépréciez toutes les anciennes clés et vous avez un tout nouveau jeu de clés, ainsi qu'un code de compatibilité qui se traduit entre les anciennes et les nouvelles clés pour le bien du code hérité.

  3. Choisissez une taille suffisante. Par défaut, le compilateur utilise le plus petit entier pouvant contenir toutes les valeurs d'énumération. Ce qui signifie que si, lors d'une mise à jour, votre ensemble d'énumérations possibles s'étend de 254 à 259, vous avez soudainement besoin de 2 octets pour chaque valeur d'énumération au lieu d'un. Cela pourrait casser la structure et les dispositions de classe partout, essayez donc d'éviter cela en utilisant une taille suffisante dans la première conception. C ++ 11 vous donne beaucoup de contrôle ici, mais sinon, spécifier une entrée LAST_SPECIES_VALUE=65535devrait également aider.

  4. Avoir un registre central. Étant donné que vous souhaitez corriger le mappage entre les noms et les valeurs, il est mauvais si vous laissez des utilisateurs tiers de votre code ajouter de nouvelles constantes. Aucun projet utilisant votre code ne doit être autorisé à modifier cet en-tête pour ajouter de nouveaux mappages. Au lieu de cela, ils devraient vous embêter pour les ajouter. Cela signifie que pour l'exemple humain et nain que vous avez mentionné, les énumérations sont en effet mal adaptées. Là, il serait préférable d'avoir une sorte de registre au moment de l'exécution, où les utilisateurs de votre code peuvent insérer une chaîne et récupérer un numéro unique, bien emballé dans un type opaque. Le «nombre» pourrait même être un pointeur vers la chaîne en question, cela n'a pas d'importance.

Malgré le fait que mon dernier point ci-dessus rend les énumérations mal adaptées à votre situation hypothétique, votre situation réelle où certaines spécifications pourraient changer et vous devrez mettre à jour du code semble correspondre assez bien avec un registre central pour votre bibliothèque. Les énumérations devraient donc être appropriées si vous prenez à cœur mes autres suggestions.

MvG
la source