web-dev-qa-db-fra.com

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

Considérez l'exemple ci-dessous. Toute modification apportée à 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 );
};

En outre, 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 dans 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.

18

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 fragile. Par exemple:

  • setColor () n'a pas à perdre de temps à vérifier si value est 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 crois que le enum class la fonctionnalité 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, l'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. Peut-être que vous recherchez 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".

25
Ixrec

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

Non, ce n'est pas le cas. Il y a deux cas: les implémenteurs

  • 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 de l'implémenteur.

Si vous mettez "Sphère", "Rectangle" et "Pyramide" dans une énumération "Forme", et passez une telle énumération à une fonction drawSolid() que vous avez écrite pour dessiner le solide correspondant, puis une le matin où vous décidez d'ajouter une valeur "Ikositetrahedron" à l'énumération, vous ne pouvez pas vous attendre à ce que la fonction drawSolid() 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 fonction drawSolid() contient une instruction switch à laquelle il manque une clause case 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 " Solid "classe avec une méthode abstraite (pure virtuelle) draw(), puis vous ajoutez les sous-classes" Sphere "," Rectangle "et" Pyramid ", chacune ayant sa propre implémentation de draw() qui sait se dessiner.

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

15
Mike Nakis

Les énumérations ne créent pas d'interfaces fragiles. Une mauvaise utilisation 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 System.DayOfWeek De .Net) À moins que vous n'ayez affaire à un calendrier incroyablement obscur, il n'y aura jamais 7 jours sur 7.
  • Couleurs non extensibles: (selon System.ConsoleColor De .Net) Certains peuvent être en désaccord avec cela, mais .Net a choisi de le faire pour un 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é qu'en le modèle de thread Java Java, il n’y aura jamais qu’un ensemble d’états fixe dans lequel un Thread peut être, et donc pour simplifier les choses, ces différents états sont représentés comme des énumérations. 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 System.Text.RegularExpressions.RegexOptions De .Net) Les Bitflags sont une utilisation très courante des énumérations. Si commun en fait, que dans .Net, toutes les énumérations ont une méthode HasFlag(Enum flag) intégrée. Elles prennent également en charge les opérateurs au niveau du bit et il existe un FlagsAttribute pour marquer une énumération comme étant destinée à être utilisée comme un ensemble de bitflags. 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 plus, les énumérations ne doivent pas être utilisées pour le personnage parce que 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 de 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 des 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 d'état 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 Strategy ), puis modifie son état en modifiant lequel à condition que la mise en œuvre soit 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, alors 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 l'absence d'un indicateur comme la condition alternative si possible (c'est-à-dire que l'absence de l'indicateur True implique False). Ce comportement peut être davantage pris en charge grâce à l'utilisation de fonctions spécialisées (c'est-à-dire IsTrue(flags) et IsFalse(flags)).
14
Pharap

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

Une ou deux d'entre elles sont correctes, mais dès que vous avez ce genre d'énumération significative, ces instructions switch ont tendance à se multiplier comme des tribbles. 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. Ce est fragile. Des sources telles que Clean Code suggèreront que vous devriez avoir au maximum un switch par énumération.

Dans ce cas, vous devez plutôt utiliser OO principes et créer une interface pour ce type. Vous pouvez conservez l'énumération pour communiquer ce type, mais dès que vous devez en faire quelque chose, vous créez un objet associé à l'énumération, éventuellement à l'aide d'une fabrique. Ceci est beaucoup plus facile à gérer, car il vous suffit d'en chercher un endroit à mettre à jour: votre Usine, et en ajoutant une nouvelle classe.

4
congusbongus

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 des valeurs. Si vous avez à un moment donné une valeur enum dans votre liste, cette valeur doit être associée à ce nom pour l'éternité. Si vous le souhaitez, vous pouvez à un moment donné renommer les valeurs en deprecated_orc ou 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 une erreur appropriée ou assurez-vous qu'aucune valeur de ce type 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=65535 devrait é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 auriez à 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.

1
MvG