Étant donné un troupeau de chevaux, comment puis-je trouver la longueur moyenne de la corne de toutes les licornes?

30

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 HornMeasurerpar 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?

moarboilerplate
la source
22
Les chevaux n'ont pas de cornes, la moyenne n'est donc pas définie (0/0).
Scott Whitlock
3
@moarboilerplate N'importe où de 10 à l'infini.
nounou
4
@StephenP: Cela ne fonctionnerait pas mathématiquement dans ce cas; tous ces 0 fausseraient la moyenne.
Mason Wheeler
3
S'il est préférable de répondre à votre question par une non-réponse, elle n'appartient pas à un site de questions / réponses; reddit, quora ou d'autres sites basés sur des discussions sont construits pour des choses de type non-réponse ... cela dit, je pense que cela peut être clairement répondu si vous recherchez le code donné par @MasonWheeler, sinon je pense que je n'ai aucune idée ce que vous essayez de demander ..
Jimmy Hoffa
3
@JimmyHoffa "vous vous trompez" se trouve être une "non-réponse" acceptable et souvent mieux que "eh bien, voici une façon de le faire" - aucune discussion approfondie n'est requise.
moarboilerplate

Réponses:

11

En supposant que vous souhaitiez traiter un Unicorncomme un type spécial de Horse, 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 des Unicorntraits. 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):

case class Horse(val hornLength: Option[Double])

val horse = Horse(None)
val unicorn = Horse(Some(12.0))
val anotherUnicorn = Horse(Some(6.0))

val herd = List(horse, unicorn, anotherUnicorn)
val hornLengths = herd flatMap {_.hornLength}
val averageLength = hornLengths.sum / hornLengths.size

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 flatMapfiltrer facilement les Noneéléments.

Karl Bielefeldt
la source
7
Bien sûr, cela suppose que la seule différence entre un cheval ordinaire et une licorne est la corne. Si ce n'est pas le cas, les choses se compliquent beaucoup plus rapidement.
Mason Wheeler
@MasonWheeler uniquement dans la deuxième méthode présentée.
moarboilerplate
1
Continuez avec les commentaires sur la façon dont les non-licornes et les licornes ne devraient jamais être écrites ensemble dans un scénario d'héritage jusqu'à ce que vous soyez dans un contexte où vous ne vous souciez pas des licornes. Bien sûr, .OfType () peut résoudre le problème et faire fonctionner les choses, mais il résout un problème qui ne devrait même pas exister en premier lieu. Quant à la deuxième approche, elle fonctionne parce que les options sont bien supérieures à compter sur null pour impliquer quelque chose. Je pense que la deuxième approche peut être réalisée en OO avec un compromis si vous encapsulez les traits de la licorne dans une propriété autonome et êtes extrêmement vigilant.
moarboilerplate
1
compromis si vous encapsulez les traits de la licorne dans une propriété autonome et êtes extrêmement vigilant - pourquoi vous rendre la vie difficile. Utilisez directement typeof et enregistrez une tonne de futurs numéros.
gbjbaanb
@gbjbaanb Je considérerais que cette approche n'est vraiment appropriée que pour les scénarios où un anémique Horsea une IsUnicornpropriété et une sorte de UnicornStuffpropriété avec la longueur du klaxon (lors de la mise à l'échelle du cavalier / paillettes mentionné dans votre question).
moarboilerplate
38

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.

Mason Wheeler
la source
Veuillez me pardonner si ma syntaxe lambda n'est pas correcte; Je ne suis pas vraiment un codeur C # et je ne peux jamais garder des détails obscurs comme celui-ci. Cela devrait être clair, cependant.
Mason Wheeler
1
Pas de soucis, le problème est à peu près résolu une fois que la liste ne contient de Unicorntoute façon que s (pour mémoire vous pourriez omettre return).
moarboilerplate
4
C'est la réponse que j'irais si je voulais résoudre le problème rapidement. Mais pas la réponse si je voulais refactoriser le code pour qu'il soit plus plausible.
Andy
6
C'est certainement la réponse, sauf si vous avez besoin d'un niveau d'optimisation absurde. La clarté et la lisibilité de celui-ci rend à peu près tout le reste discutable.
David dit Réintégrer Monica le
1
@DavidGrinberg que se passe-t-il si l'écriture de cette méthode propre et lisible signifiait que vous deviez d'abord implémenter une structure d'héritage qui n'existait pas auparavant?
moarboilerplate
9

Il n'y a rien de mal dans .NET avec:

var unicorn = animal as Unicorn;
if(unicorn != null)
{
    sum += unicorn.HornLength;
    count++;
}

L'utilisation de l'équivalent Linq est également très bien:

var averageUnicornHornLength = animals
    .OfType<Unicorn>()
    .Select(x => x.HornLength)
    .Average();

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:

var averageHornedAnimalHornLength = animals
    .OfType<IHornedAnimal>()
    .Select(x => x.HornLength)
    .Average();

Notez que lors de l'utilisation de Linq, Average(et Minet Max) 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:

var hornedAnimals = animals
    .OfType<IHornedAnimal>()
    .ToList();
if(hornedAnimals.Count > 0)
{
    var averageHornLengthOfHornedAnimals = hornedAnimals
        .Average(x => x.HornLength);
}
else
{
    // deal with it in your own way...
}

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.

Scott Whitlock
la source
7
Vous savez déjà que animal c'est un Unicorn ; il suffit de lancer plutôt que d'utiliser as, ou potentiellement une meilleure utilisation as , puis de vérifier la valeur null.
Philip Kendall
3

Mais le fait que vous finissiez par interroger le type d'un objet au moment de l'exécution ne me convient pas.

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 typeofpar 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 ifdé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)

gbjbaanb
la source
Dans un contexte hérité, if horn > 0c'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, et horn > 0sont 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"
moarboilerplate
@moarboilerplate vous le dites vous-même, optez pour la solution simple et bon marché et cela deviendra un gâchis. C'est pourquoi les langages OO ont été inventés, comme solution à ce genre de problème. Le sous-classement des chevaux peut sembler coûteux au début, mais il est vite rentabilisé. Poursuivre avec la solution simple, mais boueuse, coûte de plus en plus dans le temps.
gbjbaanb
3

Étant donné que la question a une functional-programmingbalise, 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 #:

type Equine =
| Horse
| Unicorn of hornLength: float

module equines =

  let averageHornLength (equines : Equine list) =
    equines 
    |> List.choose (fun x -> 
      match x with
      | Unicorn u -> Some(u)
      | _ -> None)
    |> List.average

let herd = [ Horse ; Horse ; Unicorn(35.0) ; Horse ; Unicorn(50.0) ]

printfn "Average horn length in herd : %f" (equines.averageHornLength herd) // prints 42.5

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 Equineapparaît un jour.

guillaume31
la source
2

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

Horse.visit(EquineVisitor v)

L'interface visiteur équine ressemble à ceci en java / c #

interface EquineVisitor {
  void visitHorse(Horse z);
  void visitUnicorn(Unicorn z);
}

Unicorn.visit(EquineVisitor v){
   v.visitUnicorn(this);
}

Horse.visit(EquineVisitor v){
   v.visitHorse(this);
}

Pour mesurer les cornes, nous écrivons maintenant ....

class HornMeasurer implements EquineVistor {
    void visitHorse(Horse h){} // ignore horses
    void visitUnicorn(Unicorn u){
         double len = u.getHornLength();
         totalLength+=len;
         unicornCount++;
    }

    double getAverageLength(){
          return totalLength/unicornCount;
    }

    double totalLength=0;
    int unicornCount=0;
}

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.

Tim Williscroft
la source
J'étais sur le point de répondre moi-même avec le modèle de visiteur. J'ai dû faire défiler une façon surprenante de trouver si quelqu'un l'avait déjà mentionné!
Ben Thurley
0

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 Horsecertains d'entre eux Unicorn, 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:

foreach (var horse in horses)
{
    try
    {
        var length = horse.MeasureHorn();
        //...
    }
    catch (NoHornException e)
    {
        continue;
    }
}

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 ifsemble 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:

class Horse
{
    public double MeasureHorn() { return -1; }
    //...
}

class Unicorn : Horse
{
    public override double MeasureHorn { return _hornLength; }
    //...
}

Maintenant, votre Horseclasse doit être au courant de la Unicornclasse et avoir des méthodes supplémentaires pour faire face aux choses qui ne l'intéressent pas. Imaginez maintenant que vous avez également des Pegasuss et des Zebras qui héritent Horse. A maintenant Horsebesoin d'une Flyméthode aussi bien que MeasureWings, CountStripesetc. Et puis la Unicornclasse 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 à Horses pour dire si quelque chose est un Unicornet 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> unicornsqui 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 comme List<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.

Becuzz
la source
L'exception silencieuse est définitivement mauvaise - ma proposition était un contrôle qui serait if(horse.IsUnicorn) horse.MeasureHorn();et les exceptions ne seraient pas interceptées - elles seraient déclenchées lorsque !horse.IsUnicornet dans un contexte de mesure de licorne, ou à l'intérieur MeasureHornsur 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.
moarboilerplate
0

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:

  1. Les valeurs de ces types sont des représentations ou des descriptions de chevaux et de licornes (respectivement);
  2. 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:

  1. 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;
  2. 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:

  1. Votre modèle doit avoir un Horsetype;
  2. Le Horsetype doit coder suffisamment d'informations pour déterminer sans ambiguïté si une valeur décrit une licorne;
  3. Certaines opérations du Horsetype doivent exposer ces informations afin que les clients du type puissent observer si un donné Horseest une licorne;
  4. 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).

sacundim
la source
-1

Le commentaire d'OP dans une autre réponse a clarifié la question, je pensais

cela fait aussi partie de la question. Si j'ai un troupeau de chevaux, et certains d'entre eux sont conceptuellement des licornes, comment devraient-ils exister pour que le problème puisse être résolu proprement sans trop d'impacts négatifs?

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:

  • Nos installations linguistiques. Par exemple, j'aborderais probablement cela différemment en rubis et javascript et Java.
  • Les concepts eux-mêmes: qu'est - ce qu'un cheval et qu'est - ce qu'une licorne? Quelles données sont associées à chacune? Sont-ils exactement les mêmes sauf pour la corne, ou ont-ils d'autres différences?
  • Comment les utilisons-nous autrement, en dehors de la moyenne des longueurs de corne? Et les troupeaux? Peut-être que nous devrions aussi les modéliser? Les utilisons-nous ailleurs? herd.averageHornLength()semble correspondre à notre modèle conceptuel.
  • Comment sont créés les objets cheval et licorne? Changer ce code est-il dans les limites de notre refactoring?

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

Jonas
la source
1
Points justes. Pour éviter que le problème ne devienne encore plus abstrait, nous devons faire des hypothèses raisonnables: 1) un langage fortement typé 2) le troupeau contraint les chevaux à un type, probablement en raison d'une collection 3) des techniques comme le typage du canard devraient probablement être évitées . Quant à ce qui peut être changé, il n'y a pas nécessairement de limites, mais chaque type de changement a ses propres conséquences ...
moarboilerplate
Si le troupeau contraint les chevaux à un seul type, ce ne sont pas nos seuls choix d'héritage (n'aime pas cette option) ou un objet wrapper (disons 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 ). HerdMemberest alors libre d'implémenter isUnicorn()comme bon lui semble, et la solution de filtrage que je propose suit.
Jonah
Dans certaines langues, hornLength () peut être mélangé, et si c'est le cas, cela peut être une solution valide. Cependant, dans les langues où la frappe est moins flexible, vous devez recourir à des techniques de piratage pour faire la même chose, ou vous devez faire quelque chose comme mettre la longueur de corne sur un cheval où cela peut entraîner une confusion dans le code parce qu'un cheval ne fait pas '' t conceptuellement avoir des cornes. En outre, si vous effectuez des calculs mathématiques, y compris les valeurs par défaut peuvent fausser les résultats (voir les commentaires sous la question d'origine)
moarboilerplate
Les mixins, cependant, à moins qu'ils ne soient exécutés, ne sont que l'héritage sous un autre nom. Votre commentaire "un cheval n'a conceptuellement pas de cornes" se rapporte à mon commentaire sur le besoin d'en savoir plus sur ce qu'ils sont, si notre réponse doit inclure comment nous modélisons les chevaux et les licornes et quelle est leur relation les uns avec les autres. Toute solution qui inclut des valeurs par défaut est incontrôlable imo.
Jonah
Vous avez raison en ce que pour obtenir une solution précise pour une manifestation spécifique de ce problème, vous devez avoir beaucoup de contexte. Pour répondre à votre question sur un cheval avec une corne et pour le rattacher aux mixins, je pensais à un scénario où une longueur de corne mélangée à un cheval qui n'est pas une licorne est une erreur. Considérez un trait Scala qui a une implémentation par défaut pour hornLength qui lève une exception. Un type de licorne peut remplacer cette implémentation, et si un cheval arrive dans un contexte où hornLength est évalué, c'est une exception.
moarboilerplate
-2

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.

Martin Maat
la source
Je suis d'accord avec ça. Mason Wheeler a également une bonne solution dans sa réponse, mais si vous avez besoin de distinguer des licornes pour de nombreuses raisons différentes à différents endroits, votre code aura beaucoup de horses.ofType<Unicorn>...constructions. Avoir une GetUnicornsfonction 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.
Shaz
@Ryan Si vous retournez un 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 le Horse. S'il Unicorns'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.
moarboilerplate
1
@moarboilerplate: Nous considérons que tout cela soutient la solution. La partie beauté est qu'elle est indépendante de tout détail de mise en œuvre de la licorne. Que vous discriminiez en fonction d'un membre de données, d'une classe ou d'une heure de la journée (ces chevaux peuvent tous se transformer en licornes à minuit si la lune est bonne pour autant que je sache), la solution est valable, l'interface reste la même.
Martin Maat