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.
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:
value
est une valeur de couleur valide ou non. Le compilateur l'a déjà fait.enum class
la fonctionnalité du C++ moderne vous permet même de forcer les gens à toujours écrire le premier au lieu du second.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".
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.
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:
Bonnes utilisations des énumérations:
System.DayOfWeek
De .Net) À moins que vous n'ayez affaire à un calendrier incroyablement obscur, il n'y aura jamais 7 jours sur 7.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.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.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:
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)
).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 switch
es 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.
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.
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.
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é.
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.
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.