Comment gérer les méthodes qui ont été ajoutées pour les sous-types dans le contexte du polymorphisme?

14

Lorsque vous utilisez le concept de polymorphisme, vous créez une hiérarchie de classes et en utilisant la référence des parents, vous appelez les fonctions d'interface sans savoir quel type spécifique a l'objet. C'est bien. Exemple:

Vous avez une collection d'animaux et vous faites appel à la fonction de tous les animaux eatet peu vous importe si c'est un chien qui mange ou un chat. Mais dans la même hiérarchie de classes, vous avez des animaux qui en ont d'autres - autres que ceux hérités et implémentés de la classe Animal, par exemple makeEggs, getBackFromTheFreezedStateetc. Donc, dans certains cas, dans votre fonction, vous voudrez peut-être connaître le type spécifique pour appeler des comportements supplémentaires.

Par exemple, au cas où il est l'heure du matin et si ce n'est qu'un animal, alors vous appelez eat, sinon si c'est un humain, alors appelez d'abord washHands, getDressedet ensuite seulement appelez eat. Comment gérer ces cas? Le polymorphisme meurt. Vous devez trouver le type d'objet, qui ressemble à une odeur de code. Existe-t-il une approche commune pour gérer ces cas?

Narek
la source
7
Le type de polymorphisme que vous avez décrit est appelé polymorphisme de sous-typage , mais ce n'est pas le seul type (voir Polymorphisme ). Vous n'avez pas besoin de créer une hiérarchie de classes pour faire du polymorphisme (et je dirais en fait que l'héritage n'est pas la méthode la plus courante pour obtenir le polymorphisme de sous-typage, l'implémentation d'une interface est beaucoup plus répandue).
Vincent Savard
24
Si vous définissez une Eaterinterface avec la eat()méthode, alors en tant que client, vous ne vous souciez pas qu'une Humanimplémentation qu'elle doive d'abord appeler washHands()et getDressed(), c'est un détail d'implémentation de cette classe. Si, en tant que client, vous vous souciez de ce fait, vous n'utilisez probablement pas le bon outil pour le travail.
Vincent Savard
3
Vous devez également considérer que, bien que le matin, un humain puisse avoir besoin de le faire getDressedavant eat, ce n'est pas le cas pour le déjeuner. Selon votre situation, cela washHands();if !dressed then getDressed();[code to actually eat]pourrait être le meilleur moyen de mettre en œuvre cela pour un humain. Une autre possibilité est que faire si d'autres choses l'exigent washHandset / ou getDressedsont appelées? Supposez que vous l'ayez leaveForWork? Vous devrez peut-être structurer le flux de votre programme pour qu'il soit tel qu'il soit appelé bien avant cela de toute façon.
Duncan X Simpson
1
Gardez à l'esprit que la vérification par rapport au type exact peut être une odeur de code dans la POO, mais c'est une pratique très courante dans la PF (c'est-à-dire utiliser la correspondance de modèle pour déterminer le type d'une union discriminée et ensuite agir dessus).
Theodoros Chatzigiannakis
3
Méfiez-vous des exemples de hiérarchies OO en salle de classe comme les animaux. Les vrais programmes n'ont presque jamais de taxonomies aussi propres. Par exemple, ericlippert.com/2015/04/27/wizards-and-warriors-part-one . Ou si vous voulez vous lancer dans le porc et remettre en question tout le paradigme: la programmation orientée objet est mauvaise .
jpmc26

Réponses:

18

Dépend. Malheureusement, il n'y a pas de solution générique. Pensez à vos besoins et essayez de comprendre ce que ces choses devraient faire.

Par exemple, vous avez dit le matin que différents animaux faisaient des choses différentes. Que diriez - vous vous présenter une méthode getUp()ou prepareForDay()quelque chose comme ça. Ensuite, vous pouvez continuer avec le polymorphisme et laisser chaque animal exécuter sa routine matinale.

Si vous souhaitez différencier les animaux, vous ne devez pas les stocker sans distinction dans une liste.

Si rien d'autre ne fonctionne, vous pouvez essayer le modèle de visiteur , qui est une sorte de hack pour permettre une sorte de répartition dynamique où vous pouvez soumettre un visiteur qui recevra des rappels de type exact des animaux. J'insiste cependant sur le fait que cela devrait être un dernier recours si tout le reste échoue.

Robert Bräutigam
la source
33

C'est une bonne question et c'est le genre de problème que beaucoup de gens essaient de comprendre comment utiliser OO. Je pense que la plupart des développeurs ont du mal avec cela. J'aurais aimé pouvoir dire que la plupart l'ont dépassé, mais je ne suis pas sûr que ce soit le cas. La plupart des développeurs, d'après mon expérience, finissent par utiliser des sacs de propriétés pseudo-OO .

Tout d'abord, permettez-moi d'être clair. Ce n'est pas ta faute. La façon dont OO est généralement enseignée est très imparfaite. L' Animalexemple est le premier délinquant, l'OMI. Fondamentalement, nous disons, parlons des objets, que peuvent-ils faire. Une Animalboîte eat()et ça peut speak(). Super. Créez maintenant des animaux et codez comment ils mangent et parlent. Maintenant, vous savez OO, non?

Le problème est que cela arrive à OO dans la mauvaise direction. Pourquoi y a-t-il des animaux dans ce programme et pourquoi ont-ils besoin de parler et de manger?

J'ai du mal à penser à une utilisation réelle d'un Animaltype. Je suis sûr qu'il existe, mais discutons de quelque chose qui, à mon avis, est plus facile à raisonner: une simulation du trafic. Supposons que nous voulons modéliser le trafic dans divers scénarios. Voici quelques éléments de base dont nous avons besoin pour pouvoir le faire.

Vehicle
Road
Signal

Nous pouvons aller plus loin avec toutes sortes de choses pour les piétons et les trains, mais nous resterons simples.

Voyons Vehicle. De quelles capacités le véhicule a-t-il besoin? Il doit voyager sur une route. Il doit pouvoir s'arrêter aux signaux. Il doit pouvoir naviguer dans les intersections.

interface Vehicle {
  move(Road road);
  navigate(Road... intersection);
}

C'est probablement trop simple mais c'est un début. Maintenant. Et toutes les autres choses qu'un véhicule pourrait faire? Ils peuvent quitter une route et devenir un fossé. Cela fait-il partie de la simulation? Non, je n'en ai pas besoin. Certaines voitures et autobus ont un système hydraulique qui leur permet de rebondir ou de s'agenouiller respectivement. Cela fait-il partie de la simulation? Non, je n'en ai pas besoin. La plupart des voitures brûlent de l'essence. Certains non. La centrale électrique fait-elle partie de la simulation? Non, je n'en ai pas besoin. Taille de roue? Je n'en ai pas besoin. Navigation GPS? Système d'infodivertissement? Je n'en ai pas besoin.

Il vous suffit de définir les comportements que vous allez utiliser. À cette fin, je pense qu'il est souvent préférable de créer des interfaces OO à partir du code qui interagit avec elles. Vous commencez avec une interface vide, puis commencez à écrire le code qui appelle les méthodes inexistantes. C'est ainsi que vous savez de quelles méthodes vous avez besoin sur votre interface. Ensuite, une fois que vous avez fait cela, vous allez commencer à définir des classes qui implémentent ces comportements. Les comportements non utilisés ne sont pas pertinents et n'ont pas besoin d'être définis.

L'intérêt de OO est que vous pouvez ajouter de nouvelles implémentations de ces interfaces plus tard sans changer le code appelant. La seule façon de fonctionner est si les besoins du code appelant déterminent ce qui se passe dans l'interface. Il n'y a aucun moyen de définir tous les comportements de toutes les choses possibles qui pourraient être imaginées plus tard.

JimmyJames
la source
13
C'est une bonne réponse. "À cette fin, je pense qu'il est souvent préférable de créer des interfaces OO à partir du code qui interagit avec elles." Absolument, et je dirais que c'est la seule façon. Vous ne pouvez pas connaître le contrat public d'une interface uniquement par la mise en œuvre, il est toujours défini du point de vue de ses clients. (Et en guise de remarque, c'est de cela qu'il s'agit réellement pour TDD.)
Vincent Savard
@VincentSavard "Je dirais que c'est la seule façon." Tu as raison. Je suppose que la raison pour laquelle je ne l'ai pas rendu aussi absolu est qu'une fois que vous avez l'idée, vous pouvez étoffer l'interface puis l'affiner de cette façon. En fin de compte, lorsque vous vous attelez aux punaises en laiton, c'est la seule chose qui compte.
JimmyJames
@ jpmc26 Peut-être un peu fortement formulé. Je ne suis pas sûr d'être d'accord qu'il est rare de mettre en œuvre cela. Je ne sais pas comment les interfaces peuvent être utiles si vous ne les utilisez pas de cette façon, à part les interfaces de marqueur qui, je pense, sont une idée terrible.
JimmyJames
9

TL; DR:

Pensez à une abstraction et à des méthodes qui s'appliquent à toutes les sous-classes et couvrent tout ce dont vous avez besoin.

Restons d'abord avec votre eat()exemple.

C'est une propriété d'être un humain qui, comme condition préalable à l'alimentation, veut se laver les mains et s'habiller avant de manger. Si vous voulez que quelqu'un vienne prendre votre petit déjeuner avec vous, vous ne leur dites pas de se laver les mains et de s'habiller, ils le font seuls quand vous les invitez, ou ils répondent "Non, je ne peux pas venir fini, je ne me suis pas lavé les mains et je ne suis pas encore habillé ".

Retour au logiciel:

En Humanexemple ne mange pas sans conditions préalables, j'aurais la Humande » eat()méthode faire washHands()et getDressed()si cela n'a pas été fait. Ce ne devrait pas être votre travail en tant eat()qu'appelant de connaître cette particularité. L'alternative de l'homme têtu serait de lever une exception ("Je ne suis pas prêt à manger!") Si les conditions préalables ne sont pas remplies, vous laissant frustré, mais au moins informé que manger n'a pas fonctionné.

Et alors makeEggs()?

Je recommanderais de changer votre façon de penser. Vous voulez probablement exécuter les tâches matinales prévues de tous les êtres. Encore une fois, en tant qu'appelant, ce ne devrait pas être votre travail de savoir quelles sont leurs fonctions. Je recommanderais donc une doMorningDuties()méthode que toutes les classes implémentent.

Ralf Kleberhoff
la source
Je suis d'accord avec cette réponse. Narek a raison sur l'odeur du code. C'est la conception de l'interface qui sent mauvais, alors corrigez cela et votre bien.
Jonathan van de Veen
Ce que cette réponse décrit est généralement appelé le principe de substitution de Liskov .
Philipp
2

La réponse est assez simple.

Comment gérer des objets qui peuvent faire plus que ce que vous attendez?

Vous n'avez pas besoin de le gérer car cela ne servirait à rien. Une interface est généralement conçue en fonction de la façon dont elle va être utilisée. Si votre interface ne définit pas le lavage des mains, alors vous ne vous en souciez pas en tant qu'appelant d'interface; si vous l'aviez fait, vous l'auriez conçu différemment.

Par exemple, si c'est le matin et si ce n'est qu'un animal, vous appelez manger, sinon si c'est un humain, appelez d'abord washHands, getDressed et ensuite seulement appelez eat. Comment gérer ces cas?

Par exemple, en pseudocode:

interface IEater { void Eat(); }
interface IMorningRoutinePerformer { void DoMorningRoutine(); }
interface IAnimal : IEater, IMorningPerformer;
interface IHuman : IEater, IMorningPerformer; 
{
  void WashHands();
  void GetDressed();
}

void MorningTime()
{
   IList<IMorningRoutinePerformer> items = Service.GetMorningPerformers();
   foreach(item in items) { item.DoMorningRoutine(); }
}

Maintenant, vous implémentez IMorningPerformerpour Animalsimplement effectuer des repas, et pour Humanvous également pour vous laver les mains et vous habiller. L'appelant de votre méthode MorningTime peut s'en moquer s'il est humain ou animal. Tout ce qu'il veut, c'est la routine matinale effectuée, ce que chaque objet fait admirablement grâce à OO.

Le polymorphisme meurt.

Ou alors?

Vous devez trouver le type d'objet

Pourquoi supposez-vous cela? Je pense que cela pourrait être une fausse hypothèse.

Existe-t-il une approche commune pour gérer ces cas?

Oui, il est généralement résolu avec une hiérarchie de classe ou d'interface soigneusement conçue. Notez que dans l'exemple ci-dessus, rien ne contredit votre exemple tel que vous l'avez donné, mais vous vous sentirez probablement insatisfait, car vous avez fait plus d'hypothèses que vous n'avez pas écrit dans la question au moment de l'écriture. , et ces hypothèses sont probablement violées.

Il est possible d'aller dans un terrier de lapin en resserrant vos hypothèses et en modifiant la réponse pour toujours les satisfaire, mais je ne pense pas que ce serait utile.

La conception de bonnes hiérarchies de classes est difficile et nécessite beaucoup d'informations sur votre domaine d'activité. Pour les domaines complexes, on passe sur deux, trois ou même plusieurs itérations, car ils affinent leur compréhension de la façon dont les différentes entités de leur domaine d'activité interagissent, jusqu'à ce qu'ils arrivent à un modèle adéquat.

C'est là que les exemples d'animaux simplistes font défaut. Nous voulons enseigner de manière simple, mais le problème que nous essayons de résoudre n'est pas évident tant que vous n'approfondissez pas, c'est-à-dire que vous avez des considérations et des domaines plus complexes.

Andrew Savinykh
la source