La question ci-dessus est un exemple abstrait d'un problème commun que je rencontre dans le code hérité, ou plus précisément, des problèmes résultant de tentatives précédentes pour résoudre ce problème.
Je peux penser à au moins une méthode de framework .NET qui est destinée à résoudre ce problème, comme la Enumerable.OfType<T>
méthode. Mais le fait que vous finissiez par interroger le type d'un objet au moment de l'exécution ne me convient pas.
Au-delà de demander à chaque cheval "Êtes-vous une licorne?" les approches suivantes viennent également à l'esprit:
- Lance une exception lorsqu'une tentative est faite pour obtenir la longueur d'une corne non licorne (expose une fonctionnalité non appropriée pour chaque cheval)
- Retourne une valeur par défaut ou magique pour la longueur d'une corne de non-licorne (nécessite des vérifications par défaut réparties dans tout code qui veut crunch les statistiques de corne sur un groupe de chevaux qui pourraient tous être des non-licornes)
- Supprimez l'héritage et créez un objet séparé sur un cheval qui vous indique si le cheval est une licorne ou non (ce qui pousse potentiellement le même problème sur une couche)
J'ai le sentiment qu'il vaudrait mieux répondre par une «non-réponse». Mais comment abordez-vous ce problème et si cela dépend, quel est le contexte autour de votre décision?
Je serais également intéressé à savoir si ce problème existe toujours dans le code fonctionnel (ou peut-être qu'il n'existe que dans les langages fonctionnels qui prennent en charge la mutabilité?)
Cela a été signalé comme un doublon possible de la question suivante: Comment éviter la rétrogradation?
La réponse à cette question suppose que l'on est en possession d'un HornMeasurer
par lequel toutes les mesures du klaxon doivent être effectuées. Mais c'est tout à fait une imposition sur une base de code qui a été formée selon le principe égalitaire selon lequel tout le monde devrait être libre de mesurer la corne d'un cheval.
En l'absence de a HornMeasurer
, l'approche de la réponse acceptée reflète l'approche basée sur les exceptions répertoriée ci-dessus.
Il y a également eu une certaine confusion dans les commentaires sur la question de savoir si les chevaux et les licornes sont tous les deux des équidés, ou si une licorne est une sous-espèce magique de cheval. Les deux possibilités doivent être considérées - peut-être que l'une est préférable à l'autre?
la source
Réponses:
En supposant que vous souhaitiez traiter un
Unicorn
comme un type spécial deHorse
, il existe essentiellement deux façons de le modéliser. La manière la plus traditionnelle est la relation de sous-classe. Vous pouvez éviter de vérifier le type et le downcasting en refactorisant simplement votre code pour toujours garder les listes séparées dans les contextes où cela importe, et les combiner uniquement dans les contextes où vous ne vous souciez jamais desUnicorn
traits. En d'autres termes, vous l'organisez de manière à ne jamais vous retrouver dans la situation où vous devez d'abord extraire des licornes d'un troupeau de chevaux. Cela semble difficile au début, mais est possible dans 99,99% des cas, et rend généralement votre code beaucoup plus propre.L'autre façon de modéliser une licorne est simplement de donner à tous les chevaux une longueur de corne facultative. Ensuite, vous pouvez tester si c'est une licorne en vérifiant si elle a une longueur de corne, et trouver la longueur moyenne de corne de toutes les licornes par (dans Scala):
Cette méthode a l'avantage d'être plus simple, avec une seule classe, mais l'inconvénient d'être beaucoup moins extensible, et d'avoir en quelque sorte un moyen détourné de vérifier la «licorne». L'astuce si vous optez pour cette solution est de reconnaître lorsque vous commencez à l'étendre souvent que vous devez passer à une architecture plus flexible. Ce type de solution est beaucoup plus populaire dans les langages fonctionnels où vous avez des fonctions simples et puissantes comme
flatMap
filtrer facilement lesNone
éléments.la source
Horse
a uneIsUnicorn
propriété et une sorte deUnicornStuff
propriété avec la longueur du klaxon (lors de la mise à l'échelle du cavalier / paillettes mentionné dans votre question).Vous avez à peu près couvert toutes les options. Si vous avez un comportement qui dépend d'un sous-type spécifique et qu'il est mélangé à d'autres types, votre code doit être conscient de ce sous-type; c'est un simple raisonnement logique.
Personnellement, j'irais avec
horses.OfType<Unicorn>().Average(u => u.HornLength)
. Il exprime très clairement l'intention du code, ce qui est souvent la chose la plus importante car quelqu'un va finir par devoir le maintenir plus tard.la source
Unicorn
toute façon que s (pour mémoire vous pourriez omettrereturn
).Il n'y a rien de mal dans .NET avec:
L'utilisation de l'équivalent Linq est également très bien:
Sur la base de la question que vous avez posée dans le titre, voici le code que je m'attendrais à trouver. Si la question demandait quelque chose comme "quelle est la moyenne des animaux à cornes", ce serait différent:
Notez que lors de l'utilisation de Linq,
Average
(etMin
etMax
) lèvera une exception si l'énumérable est vide et que le type T n'est pas nullable. C'est parce que la moyenne n'est vraiment pas définie (0/0). Vous avez donc vraiment besoin de quelque chose comme ça:modifier
Je pense simplement que cela doit être ajouté ... l'une des raisons pour lesquelles une question comme celle-ci ne convient pas aux programmeurs orientés objet est qu'elle suppose que nous utilisons des classes et des objets pour modéliser une structure de données. L'idée originale orientée objet de Smalltalk était de structurer votre programme à partir de modules qui ont été instanciés en tant qu'objets et ont rendu des services pour vous lorsque vous leur avez envoyé un message. Le fait que nous puissions également utiliser des classes et des objets pour modéliser une structure de données est un effet secondaire (utile), mais ce sont deux choses différentes. Je ne pense même pas que cette dernière devrait être considérée comme une programmation orientée objet, car vous pourriez faire la même chose avec a
struct
, mais ce ne serait pas aussi joli.Si vous utilisez la programmation orientée objet pour créer des services qui font des choses pour vous, alors demander si ce service est en fait un autre service ou une implémentation concrète est généralement mal vu pour de bonnes raisons. On vous a donné une interface (généralement par injection de dépendance) et vous devez coder pour cette interface / contrat.
D'un autre côté, si vous utilisez (mal) les idées de classe / objet / interface pour créer une structure de données ou un modèle de données, je ne vois personnellement pas de problème avec l'utilisation de l'idée is-a au maximum. Si vous avez défini que les licornes sont un sous-type de chevaux et que cela a du sens dans votre domaine, alors allez-y absolument et interrogez les chevaux de votre troupeau pour trouver les licornes. Après tout, dans un cas comme celui-ci, nous essayons généralement de créer un langage spécifique au domaine pour mieux exprimer les solutions aux problèmes que nous devons résoudre. En ce sens, il n'y a rien de mal à
.OfType<Unicorn>()
etc.En fin de compte, prendre une collection d'éléments et la filtrer sur le type est vraiment juste une programmation fonctionnelle, pas une programmation orientée objet. Heureusement, les langages comme C # sont désormais à l'aise avec les deux paradigmes.
la source
animal
c'est unUnicorn
; il suffit de lancer plutôt que d'utiliseras
, ou potentiellement une meilleure utilisationas
, puis de vérifier la valeur null.Le problème avec cette déclaration est que, quel que soit le mécanisme que vous utilisez, vous interrogerez toujours l'objet pour savoir de quel type il s'agit. Cela peut être RTTI ou ce peut être une union ou une structure de données simple où vous demandez
if horn > 0
. Les détails exacts changent légèrement mais l'intention est la même - vous demandez à l'objet de lui-même d'une manière ou d'une autre pour voir si vous devez l'interroger davantage.Cela étant, il est logique d'utiliser le support de votre langue pour ce faire. Dans .NET, vous utiliseriez
typeof
par exemple.La raison de cela va au-delà de la simple utilisation de votre langue. Si vous avez un objet qui ressemble à un autre mais pour un petit changement, il est probable que vous constaterez plus de différences au fil du temps. Dans votre exemple de licornes / chevaux, vous pouvez dire qu'il n'y a que la longueur de la corne ... mais demain vous vérifierez si un cavalier potentiel est vierge ou si le caca est scintillant. (un exemple classique dans le monde réel serait les widgets GUI qui dérivent d'une base commune et vous devez rechercher les cases à cocher et les listes différemment. Le nombre de différences serait trop grand pour créer simplement un super-objet unique qui contiendrait toutes les permutations possibles des données ).
Si la vérification du type d'un objet à l'exécution ne tient pas bien, alors votre alternative est de diviser les différents objets dès le début - au lieu de stocker un seul troupeau de licornes / chevaux, vous détenez 2 collections - une pour les chevaux, une pour les licornes . Cela peut très bien fonctionner, même si vous les stockez dans un conteneur spécialisé (par exemple, une multi-carte où la clé est le type d'objet ... mais alors même si nous les stockons en 2 groupes, nous sommes de retour à interroger le type d'objet !)
Une approche basée sur les exceptions est certainement fausse. Utiliser des exceptions comme flux de programme normal est une odeur de code (si vous aviez un troupeau de licornes et un âne avec un coquillage collé sur sa tête, alors je dirais que l'approche basée sur les exceptions est OK, mais si vous avez un troupeau de licornes et les chevaux qui vérifient ensuite chacun pour la licorne n'est pas inattendu. Les exceptions sont pour des circonstances exceptionnelles, pas une
if
déclaration compliquée ). Dans tous les cas, l'utilisation d'exceptions pour ce problème revient simplement à interroger le type d'objet au moment de l'exécution, seulement ici vous abusez de la fonction de langage pour rechercher des objets non licornes. Vous pourriez aussi bien coder dans unif horn > 0
et au moins traiter votre collection rapidement, clairement, en utilisant moins de lignes de code et en évitant tout problème résultant de la levée d'autres exceptions (par exemple une collection vide ou en essayant de mesurer le coquillage de cet âne)la source
if horn > 0
c'est à peu près la façon dont ce problème est résolu au début. Ensuite, les problèmes qui surviennent généralement sont lorsque vous souhaitez vérifier les cavaliers et les paillettes, ethorn > 0
sont enterrés partout dans un code non lié (le code souffre également de bugs mystères en raison du manque de vérifications lorsque le klaxon est à 0). En outre, le sous-classement des chevaux après le fait est généralement la proposition la plus coûteuse, donc je ne suis généralement pas enclin à le faire s'ils sont toujours parqués ensemble à la fin du refactor. Donc, cela devient certainement "à quel point les alternatives sont laides"Étant donné que la question a une
functional-programming
balise, nous pourrions utiliser un type de somme pour refléter les deux saveurs des chevaux et la correspondance des motifs pour lever les ambiguïtés entre eux. Par exemple, en F #:Par rapport à la POO, FP a l'avantage de la séparation des données / fonctions, ce qui peut vous sauver de la "conscience coupable" (injustifiée?) De violer le niveau d'abstraction lors de la conversion vers des sous-types spécifiques d'une liste d'objets d'un super-type.
Contrairement aux solutions OO proposées dans d'autres réponses, l'appariement de motifs fournit également un point d'extension plus facile si une autre espèce de Cornu
Equine
apparaît un jour.la source
La forme courte de la même réponse à la fin nécessite la lecture d'un livre ou d'un article Web.
Modèle de visiteur
Le problème a un mélange de chevaux et de licornes. (La violation du principe de substitution de Liskov est un problème courant dans les bases de code héritées.)
Ajouter une méthode au cheval et à toutes les sous-classes
L'interface visiteur équine ressemble à ceci en java / c #
Pour mesurer les cornes, nous écrivons maintenant ....
Le modèle de visiteurs est critiqué pour rendre le refactoring et la croissance plus difficiles.
Réponse courte: Utilisez le modèle de conception Visitor pour obtenir une double expédition.
voir aussi https://en.wikipedia.org/wiki/Visitor_pattern
voir aussi http://c2.com/cgi/wiki?VisitorPattern pour la discussion des visiteurs.
voir également Design Patterns par Gamma et al.
la source
En supposant que dans votre architecture, les licornes sont une sous-espèce de cheval et que vous rencontrez des endroits où vous obtenez une collection de
Horse
certains d'entre euxUnicorn
, je choisirais personnellement la première méthode (.OfType<Unicorn>()...
) car c'est la façon la plus simple d'exprimer votre intention. . Pour ceux qui viendront plus tard (vous y compris dans 3 mois), il est immédiatement évident ce que vous essayez d'accomplir avec ce code: choisissez les licornes parmi les chevaux.Les autres méthodes que vous avez énumérées semblent être une autre façon de poser la question "Êtes-vous une licorne?". Par exemple, si vous utilisez une sorte de méthode basée sur des exceptions pour mesurer les klaxons, vous pourriez avoir un code qui ressemblerait à ceci:
Alors maintenant, l'exception devient l'indicateur que quelque chose n'est pas une licorne. Et maintenant, ce n'est plus vraiment une situation exceptionnelle , mais cela fait partie du déroulement normal du programme. Et l'utilisation d'une exception au lieu d'un
if
semble encore plus sale que la simple vérification de type.Disons que vous allez sur la voie de la valeur magique pour vérifier les cornes des chevaux. Alors maintenant, vos classes ressemblent à ceci:
Maintenant, votre
Horse
classe doit être au courant de laUnicorn
classe et avoir des méthodes supplémentaires pour faire face aux choses qui ne l'intéressent pas. Imaginez maintenant que vous avez également desPegasus
s et desZebra
s qui héritentHorse
. A maintenantHorse
besoin d'uneFly
méthode aussi bien queMeasureWings
,CountStripes
etc. Et puis laUnicorn
classe obtient aussi ces méthodes. Maintenant, vos classes doivent toutes se connaître et vous avez pollué les classes avec un tas de méthodes qui ne devraient pas être là juste pour éviter de demander au système de type "Est-ce une licorne?"Alors qu'en est-il de l'ajout de quelque chose à
Horse
s pour dire si quelque chose est unUnicorn
et de gérer toutes les mesures de cor? Eh bien, maintenant, vous devez vérifier l'existence de cet objet pour savoir si quelque chose est une licorne (qui remplace simplement un contrôle par un autre). Il brouille également un peu les eaux en ce que maintenant vous pouvez avoir unList<Horse> unicorns
qui contient vraiment toutes les licornes, mais le système de type et le débogueur ne peuvent pas facilement vous le dire. «Mais je sais que ce sont toutes des licornes», dites-vous, «le nom le dit même». Et si quelque chose était mal nommé? Ou disons, vous avez écrit quelque chose avec l'hypothèse que ce serait vraiment toutes les licornes, mais ensuite les exigences ont changé et maintenant il pourrait aussi y avoir du pegasi mélangé? (Parce que rien de tel ne se produit jamais, en particulier dans les logiciels / sarcasmes hérités.) Maintenant, le système de type mettra volontiers votre pegasi avec vos licornes. Si votre variable avait été déclarée commeList<Unicorn>
le compilateur (ou l'environnement d'exécution) aurait un ajustement si vous essayiez de mélanger pegasi ou chevaux.Enfin, toutes ces méthodes ne sont qu'un remplacement pour la vérification du système de type. Personnellement, je préfère ne pas réinventer la roue ici et espérer que mon code fonctionne aussi bien que quelque chose qui est intégré et a été testé par des milliers d'autres codeurs des milliers de fois.
En fin de compte, le code doit être compréhensible pour vous . L'ordinateur le trouvera indépendamment de la façon dont vous l'écrivez. Vous êtes celui qui doit le déboguer et être capable d'en raisonner. Faites le choix qui vous facilite la tâche. Si pour une raison quelconque, l'une de ces autres méthodes vous offre un avantage qui l'emporte sur un code plus clair dans les quelques endroits où il apparaîtrait, allez-y. Mais cela dépend de votre base de code.
la source
if(horse.IsUnicorn) horse.MeasureHorn();
et les exceptions ne seraient pas interceptées - elles seraient déclenchées lorsque!horse.IsUnicorn
et dans un contexte de mesure de licorne, ou à l'intérieurMeasureHorn
sur une non-licorne. De cette façon, lorsque l'exception est levée, vous ne masquez pas les erreurs, elle explose complètement et est un signe que quelque chose doit être corrigé. De toute évidence, il n'est approprié que pour certains scénarios, mais il s'agit d'une implémentation qui n'utilise pas de levée d'exceptions pour déterminer un chemin d'exécution.Eh bien, il semble que votre domaine sémantique ait une relation IS-A, mais vous vous méfiez un peu de l'utilisation de sous-types / héritage pour modéliser cela, en particulier en raison de la réflexion du type d'exécution. Je pense cependant que vous avez peur de la mauvaise chose - le sous-typage présente en effet des dangers, mais le fait que vous interrogiez un objet à l'exécution n'est pas le problème. Vous verrez ce que je veux dire.
La programmation orientée objet s'est appuyée assez fortement sur la notion de relations IS-A, elle s'est sans doute trop appuyée sur elle, conduisant à deux célèbres concepts critiques:
Mais je pense qu'il existe une autre façon, plus basée sur la programmation fonctionnelle, d'examiner les relations IS-A qui n'a peut-être pas ces difficultés. Tout d'abord, nous voulons modéliser les chevaux et les licornes dans notre programme, nous allons donc avoir un
Horse
et unUnicorn
type. Quelles sont les valeurs de ces types? Eh bien, je dirais ceci:Cela peut sembler évident, mais je pense que l'une des façons dont les gens abordent des problèmes comme le problème du cercle-ellipse est de ne pas s'occuper de ces points avec suffisamment d'attention. Chaque cercle est une ellipse, mais cela ne signifie pas que chaque description schématisée d'un cercle est automatiquement une description schématisée d'une ellipse selon un schéma différent. En d'autres termes, ce n'est pas parce qu'un cercle est une ellipse que a
Circle
est unEllipse
, pour ainsi dire. Mais cela signifie que:Circle
(description de cercle schématisée) enEllipse
(type de description différent) qui décrit les mêmes cercles;Ellipse
et, si décrit un cercle, renvoie le correspondantCircle
.Donc, en termes de programmation fonctionnelle, votre
Unicorn
type n'a pas besoin d'être un sous-type duHorse
tout, vous avez juste besoin d'opérations comme celles-ci:Et
toUnicorn
doit être un inverse droit detoHorse
:Le
Maybe
type de Haskell est ce que les autres langues appellent un type "option". Par exemple, leOptional<Unicorn>
type Java 8 est unUnicorn
ou rien. Notez que deux de vos alternatives - lever une exception ou renvoyer une «valeur par défaut ou magique» - sont très similaires aux types d'options.Donc, fondamentalement, ce que j'ai fait ici est de reconstruire le concept IS-A en termes de types et de fonctions, sans utiliser de sous-types ou d'héritage. Ce que j'en retiendrais, c'est:
Horse
type;Horse
type doit coder suffisamment d'informations pour déterminer sans ambiguïté si une valeur décrit une licorne;Horse
type doivent exposer ces informations afin que les clients du type puissent observer si un donnéHorse
est une licorne;Horse
type devront utiliser ces dernières opérations lors de l'exécution pour distinguer les licornes et les chevaux.Il s'agit donc fondamentalement d'un
Horse
modèle "demandez à tous s'il s'agit d'une licorne". Vous vous méfiez de ce modèle, mais je le pense à tort. Si je vous donne une liste deHorse
s, tout ce que le type garantit est que les choses que les éléments de la liste décrivent sont des chevaux - vous devrez donc inévitablement faire quelque chose au moment de l'exécution pour dire lesquels sont des licornes. Il n'y a donc pas moyen de contourner cela, je pense - vous devez mettre en œuvre des opérations qui le feront pour vous.Dans la programmation orientée objet, la façon habituelle de procéder est la suivante:
Horse
type;Unicorn
comme sous-type deHorse
;Horse
est unUnicorn
.Cela a une grande faiblesse, lorsque vous le regardez sous l'angle "chose vs description" que j'ai présenté ci-dessus:
Horse
instance qui décrit une licorne mais n'est pas uneUnicorn
instance?Pour en revenir au début, c'est ce que je pense être la partie vraiment effrayante de l'utilisation du sous-typage et des downcasts pour modéliser cette relation IS-A - pas le fait que vous devez faire une vérification de l'exécution. Abuser un peu de la typographie, demander
Horse
si c'est uneUnicorn
instance n'est pas synonyme de demanderHorse
si c'est une licorne (si c'est uneHorse
description d'un cheval qui est aussi une licorne). Sauf si votre programme a fait de grands efforts pour encapsuler le code qui construit deHorses
sorte que chaque fois qu'un client essaie de construire unHorse
qui décrit une licorne, laUnicorn
classe est instanciée. D'après mon expérience, les programmeurs font rarement cela avec soin.Je choisirais donc l'approche où il y a une opération explicite et non abattue qui convertit
Horse
s enUnicorn
s. Il peut s'agir d'une méthode duHorse
type:... ou ce pourrait être un objet extérieur (votre "objet séparé sur un cheval qui vous indique si le cheval est une licorne ou non"):
Le choix entre ceux-ci dépend de la façon dont votre programme est organisé - dans les deux cas, vous avez l'équivalent de mon
Horse -> Maybe Unicorn
opération d'en haut, vous ne faites que l'empaqueter de différentes manières (ce qui aura certainement des effets d'entraînement sur les opérations dont leHorse
type a besoin exposer à ses clients).la source
Le commentaire d'OP dans une autre réponse a clarifié la question, je pensais
Formulé de cette façon, je pense que nous avons besoin de plus d'informations. La réponse dépend probablement d'un certain nombre de choses:
herd.averageHornLength()
semble correspondre à notre modèle conceptuel.En général, cependant, je ne penserais même pas à l'héritage et aux sous-types ici. Vous avez une liste d'objets. Certains de ces objets peuvent être identifiés comme des licornes, peut-être parce qu'ils ont une
hornLength()
méthode. Filtrez la liste en fonction de cette propriété unique de licorne. Maintenant, le problème a été réduit à la moyenne de la longueur de la corne d'une liste de licornes.OP, faites-moi savoir si je ne comprends toujours pas ...
la source
HerdMember
) que nous initialisons avec un cheval ou une licorne (libérant le cheval et la licorne d'avoir besoin d'une relation de sous-type ).HerdMember
est alors libre d'implémenterisUnicorn()
comme bon lui semble, et la solution de filtrage que je propose suit.Une méthode GetUnicorns () qui renvoie un IEnumerable me semble la solution la plus élégante, flexible et universelle. De cette façon, vous pouvez gérer n'importe quel (combinaison de) traits qui déterminent si un cheval passera comme licorne, pas seulement le type de classe ou la valeur d'une propriété particulière.
la source
horses.ofType<Unicorn>...
constructions. Avoir uneGetUnicorns
fonction serait une ligne unique, mais elle serait encore plus résistante aux changements dans la relation cheval / licorne du point de vue de l'appelant.IEnumerable<Horse>
, bien que vos critères de licorne soient au même endroit, il est encapsulé, donc vos appelants doivent faire des suppositions sur la raison pour laquelle ils ont besoin de licornes (je peux obtenir une chaudrée de palourdes en commandant la soupe du jour aujourd'hui, mais cela ne fonctionne pas) Je veux dire que je l’aurai demain en faisant la même chose). De plus, vous devez exposer une valeur par défaut pour un klaxon sur leHorse
. S'ilUnicorn
s'agit de son propre type, vous devez créer un nouveau type et gérer les mappages de type, ce qui peut entraîner une surcharge.