web-dev-qa-db-fra.com

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

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 eat et 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 ont d'autres - autres qu'hérités et implémentés de la classe Animal, par exemple makeEggs, getBackFromTheFreezedState et ainsi de suite. 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, si c'est le matin et si c'est juste un animal, alors vous appelez eat, sinon si c'est un humain, alors appelez d'abord washHands, getDressed et alors 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?

14
Narek

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 d'introduire une méthode getUp() ou prepareForDay() ou 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, alors vous pouvez essayer le Visitor Pattern , qui est 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.

18
Robert Bräutigam

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. D'après mon expérience, la plupart des développeurs finissent par utiliser 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'exemple Animal est le premier contrevenant, IMO. Fondamentalement, nous disons, parlons des objets, que peuvent-ils faire. Animal can eat() and it can speak(). Super. Maintenant, créez des animaux et codez comment ils mangent et parlent. Maintenant vous savez OO, non?

Le problème est que cela vient à 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 type Animal. 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.

Prenons 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, vous allez commencer à définir des classes qui implémentent ces comportements. Les comportements qui ne sont pas 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 qui fonctionne 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.

33
JimmyJames

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 exemple eat().

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, je ne me suis pas lavé les mains et je ne suis pas encore habillé ".

Retour au logiciel:

Comme une instance de Human ne mangera pas sans les conditions préalables, je demanderais à la méthode eat() de Human de faire washHands() et getDressed() si cela n'a pas été fait. Ce ne devrait pas être votre travail en tant qu'appelant eat() 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é.

Qu'en est-il de 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 méthode doMorningDuties() que toutes les classes implémentent.

9
Ralf Kleberhoff

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 IMorningPerformer pour Animal juste pour manger, et pour Human vous l'implémentez é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 hypothèse erronée.

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, il n'y a rien qui 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 de connaissances 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 simplement, mais le problème que nous essayons de résoudre n'est pas évident tant que vous n'allez pas plus loin, c'est-à-dire que vous avez des considérations et des domaines plus complexes.

2
Andrew Savinykh