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 Horseet un Unicorntype. Quelles sont les valeurs de ces types? Eh bien, je dirais ceci:
- Les valeurs de ces types sont des représentations ou des descriptions de chevaux et de licornes (respectivement);
- Ce sont des représentations ou descriptions schématisées - elles ne sont pas de forme libre, elles sont construites selon des règles très strictes.
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 Circleest un Ellipse, pour ainsi dire. Mais cela signifie que:
- Il existe une fonction totale qui convertit n'importe quelle
Circle(description de cercle schématisée) en Ellipse(type de description différent) qui décrit les mêmes cercles;
- Il existe une fonction partielle qui prend un
Ellipseet, si décrit un cercle, renvoie le correspondant Circle.
Donc, en termes de programmation fonctionnelle, votre Unicorntype n'a pas besoin d'être un sous-type du Horsetout, vous avez juste besoin d'opérations comme celles-ci:
-- Convert any unicorn-description of into a horse-description that
-- describes the same unicorns.
toHorse :: Unicorn -> Horse
-- If the horse described by the given horse-description is a unicorn,
-- then return a unicorn-description of that unicorn, otherwise return
-- nothing.
toUnicorn :: Horse -> Maybe Unicorn
Et toUnicorndoit être un inverse droit de toHorse:
toUnicorn (toHorse x) = Just x
Le Maybetype de Haskell est ce que les autres langues appellent un type "option". Par exemple, le Optional<Unicorn>type Java 8 est un Unicornou 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:
- Votre modèle doit avoir un
Horsetype;
- Le
Horsetype doit coder suffisamment d'informations pour déterminer sans ambiguïté si une valeur décrit une licorne;
- Certaines opérations du
Horsetype doivent exposer ces informations afin que les clients du type puissent observer si un donné Horseest une licorne;
- Les clients du
Horsetype 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 Horsemodè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 de Horses, 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:
- Ayez un
Horsetype;
- Avoir
Unicorncomme sous-type de Horse;
- Utilisez la réflexion du type d'exécution comme opération accessible au client qui discerne si un donné
Horseest un Unicorn.
Cela a une grande faiblesse, lorsque vous le regardez sous l'angle "chose vs description" que j'ai présenté ci-dessus:
- Et si vous avez une
Horseinstance qui décrit une licorne mais n'est pas une Unicorninstance?
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 Horsesi c'est une Unicorninstance n'est pas synonyme de demander Horsesi c'est une licorne (si c'est une Horsedescription d'un cheval qui est aussi une licorne). Sauf si votre programme a fait de grands efforts pour encapsuler le code qui construit de Horsessorte que chaque fois qu'un client essaie de construire un Horsequi décrit une licorne, la Unicornclasse 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 Horses en Unicorns. Il peut s'agir d'une méthode du Horsetype:
interface Horse {
// ...
Optional<Unicorn> toUnicorn();
}
... 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"):
class HorseToUnicornCoercion {
Optional<Unicorn> convert(Horse horse) {
// ...
}
}
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 Unicornopé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 le Horsetype a besoin exposer à ses clients).