Existe-t-il un langage ou un modèle de conception qui permet la * suppression * du comportement ou des propriétés des objets dans une hiérarchie de classes?

28

Un inconvénient bien connu des hiérarchies de classes traditionnelles est qu'elles sont mauvaises en ce qui concerne la modélisation du monde réel. Par exemple, essayer de représenter les espèces animales avec des classes. Il y a en fait plusieurs problèmes en faisant cela, mais celui auquel je n'ai jamais vu de solution est quand une sous-classe "perd" un comportement ou une propriété qui a été définie dans une super-classe, comme un pingouin ne pouvant pas voler (il y a sont probablement de meilleurs exemples, mais c'est le premier qui me vient à l'esprit).

D'une part, vous ne voulez pas définir, pour chaque propriété et comportement, un indicateur qui spécifie s'il est présent, et le vérifier à chaque fois avant d'accéder à ce comportement ou à cette propriété. Vous voulez juste dire que les oiseaux peuvent voler, simplement et clairement, dans la classe Bird. Mais alors ce serait bien si on pouvait définir des "exceptions" par la suite, sans avoir à utiliser des horribles hacks partout. Cela se produit souvent lorsqu'un système est productif depuis un certain temps. Vous trouvez soudain une «exception» qui ne correspond pas du tout à la conception d'origine, et vous ne voulez pas modifier une grande partie de votre code pour l'adapter.

Donc, y a-t-il des modèles de langage ou de conception qui peuvent gérer proprement ce problème, sans nécessiter de changements majeurs à la "super-classe", et tout le code qui l'utilise? Même si une solution ne gère qu'un cas spécifique, plusieurs solutions peuvent former ensemble une stratégie complète.

Après réflexion, je me rends compte que j'ai oublié le principe de substitution de Liskov. Voilà pourquoi vous ne pouvez pas le faire. En supposant que vous définissiez des "traits / interfaces" pour tous les "groupes d'entités" principaux, vous pouvez librement implémenter des traits dans différentes branches de la hiérarchie, comme le trait Volant pourrait être implémenté par Birds, et certains types spéciaux d'écureuils et de poissons.

Donc, ma question pourrait se résumer à "Comment pourrais-je annuler la mise en œuvre d'un trait?" Si votre super-classe est un Java Serializable, vous devez l'être aussi, même s'il n'y a aucun moyen pour vous de sérialiser votre état, par exemple si vous contenez un "Socket".

Une façon de le faire est de toujours définir tous vos traits par paires dès le début: Flying et NotFlying (ce qui lèverait UnsupportedOperationException, s'il n'est pas vérifié). Le Not-trait ne définirait aucune nouvelle interface et pourrait être simplement vérifié. Cela ressemble à une solution "bon marché", en particulier si elle est utilisée dès le départ.

Sébastien Diot
la source
3
«sans avoir à utiliser des horribles hacks partout»: désactiver un comportement EST un horrible hack: cela impliquerait que cela function save_yourself_from_crashing_airplane(Bird b) { f.fly() }deviendrait beaucoup plus compliqué. (comme Peter Török l'a dit, cela viole le LSP)
keppla
Une combinaison du modèle de stratégie et de l'héritage pourrait vous permettre de "composer sur" un comportement hérité pour des super types spécifiques? Quand vous dites: " it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"considérez-vous une méthode d'usine contrôlant le comportement hacky?
StuperUser
1
On pourrait bien sûr simplement jeter un NotSupportedExceptionde Penguin.fly().
Felix Dombek
En ce qui concerne les langues, vous pouvez certainement annuler l'implémentation d'une méthode dans une classe enfant. Par exemple, dans Ruby: class Penguin < Bird; undef fly; end;. Si vous devez ou non est une autre question.
Nathan Long
Cela briserait le principe de liskov et sans doute tout l'intérêt de la POO.
deadalnix

Réponses:

17

Comme d'autres l'ont mentionné, il faudrait aller contre LSP.

Cependant, on peut affirmer qu'une sous-classe n'est qu'une extension arbitraire d'une super classe. C'est un nouvel objet à part entière et la seule relation avec la super classe est qu'il utilise une fondation.

Cela peut être logique, plutôt que de dire que Pingouin est un oiseau. Votre Pingouin dit hérite d'un sous-ensemble de comportement de Bird.

En général, les langages dynamiques vous permettent de l'exprimer facilement, un exemple utilisant JavaScript suit ci-dessous:

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

Dans ce cas particulier, Penguinobserve activement la Bird.flyméthode dont il hérite en écrivant une flypropriété avec une valeur undefineddans l'objet.

Maintenant, vous pouvez dire que Penguincela ne peut plus être traité comme une normale Bird. Mais comme mentionné, dans le monde réel, cela ne peut tout simplement pas. Parce que nous modélisons Birdcomme étant une entité volante.

L'alternative est de ne pas faire l'hypothèse générale que Bird's peut voler. Il serait judicieux d'avoir une Birdabstraction qui permette à tous les oiseaux d'en hériter, sans échec. Cela signifie seulement faire des hypothèses que toutes les sous-classes peuvent contenir.

Généralement, l'idée de Mixin s'applique bien ici. Avoir une classe de base très mince et y mélanger tous les autres comportements.

Exemple:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

Si vous êtes curieux, j'ai une implémentation deObject.make

Une addition:

Donc, ma question pourrait se résumer à "Comment pourrais-je annuler la mise en œuvre d'un trait?" Si votre super-classe est un Java Serializable, vous devez l'être aussi, même s'il n'y a aucun moyen pour vous de sérialiser votre état, par exemple si vous contenez un "Socket".

Vous ne "désimplémentez" pas un trait. Vous corrigez simplement votre hiérarchie d'héritage. Soit vous pouvez remplir votre contrat de super classes, soit vous ne devriez pas prétendre que vous êtes de ce type.

C'est là que la composition des objets brille.

Soit dit en passant, Serializable ne signifie pas que tout doit être sérialisé, cela signifie seulement que "l'état qui vous intéresse" doit être sérialisé.

Vous ne devez pas utiliser un trait "NotX". C'est tout simplement horrible ballonnement de code. Si une fonction attend un objet volant, il devrait s'écraser et brûler lorsque vous lui donnez un mammouth.

Raynos
la source
10
"dans le monde réel, cela ne peut tout simplement pas." Oui il peut. Un pingouin est un oiseau. La capacité de voler n'est pas une propriété des oiseaux, c'est simplement une propriété fortuite de la plupart des espèces d'oiseaux. Les propriétés qui définissent les oiseaux sont "les animaux à plumes, ailés, bipèdes, endothermiques, pondeurs, vertébrés" (Wikipedia) - rien à propos de voler là-bas.
pdr
2
@pdr encore une fois, cela dépend de votre définition de l'oiseau. Comme j'utilisais le terme «oiseau», je voulais dire l'abstraction de classe que nous utilisons pour représenter les oiseaux, y compris la méthode fly. J'ai également mentionné que vous pouvez rendre votre abstraction de classe moins spécifique. Un pingouin n'a pas non plus de plumes.
Raynos
2
@Raynos: Les pingouins sont en effet à plumes. Bien entendu, leurs plumes sont assez courtes et denses.
Jon Purdy
@ JonPurdy assez juste, j'imagine toujours qu'ils avaient de la fourrure.
Raynos
+1 en général, et en particulier pour le "mammouth". LOL!
Sebastien Diot
28

AFAIK tous les langages basés sur l'héritage sont construits sur le principe de substitution Liskov . Supprimer / désactiver une propriété de classe de base dans une sous-classe violerait clairement LSP, donc je ne pense pas qu'une telle possibilité soit implémentée n'importe où. Le monde réel est en effet désordonné et ne peut pas être modélisé avec précision par des abstractions mathématiques.

Certaines langues proposent des traits ou des mixins, précisément pour traiter ces problèmes de manière plus flexible.

Péter Török
la source
1
Le LSP est pour les types , pas pour les classes .
Jörg W Mittag
2
@ PéterTörök: Cette question n'existerait pas autrement :-) Je peux penser à deux exemples de Ruby. Classest une sous-classe de Modulemême si ClassIS-NOT-A Module. Mais il est toujours logique d'être une sous-classe, car il réutilise une grande partie du code. OTOH, StringIOIS-A IO, mais les deux n'ont pas de relation d'héritage (à part l'évidence des deux héritant de Object, bien sûr), car ils ne partagent aucun code. Les classes sont pour le partage de code, les types sont pour décrire les protocoles. IOet StringIOont le même protocole, donc le même type, mais leurs classes ne sont pas liées.
Jörg W Mittag
1
@ JörgWMittag, OK, maintenant je comprends mieux ce que tu veux dire. Cependant, pour moi, votre premier exemple ressemble plus à une mauvaise utilisation de l'héritage qu'à l'expression d'un problème fondamental que vous semblez suggérer. L'héritage public IMO ne doit pas être utilisé pour réutiliser l'implémentation, mais uniquement pour exprimer des relations de sous-type (is-a). Et le fait qu'il puisse être utilisé à mauvais escient ne le disqualifie pas - je ne peux imaginer aucun outil utilisable de n'importe quel domaine qui ne puisse pas être utilisé à mauvais escient.
Péter Török
2
Aux personnes qui votent pour cette réponse: notez que cela ne répond pas vraiment à la question, surtout après la clarification éditée. Je ne pense pas que cette réponse mérite un downvote, car ce qu'il dit est très vrai et important à savoir, mais il n'a pas vraiment répondu à la question.
jhocking
1
Imaginez un Java dans lequel seules les interfaces sont des types, les classes ne le sont pas et les sous-classes sont capables de "désimplémenter" les interfaces de leur superclasse, et vous avez une idée approximative, je pense.
Jörg W Mittag
15

Fly()est dans le premier exemple de: Head First Design Patterns for The Strategy Pattern , et c'est une bonne situation pour savoir pourquoi vous devriez «privilégier la composition plutôt que l'héritage». .

Vous pouvez mélanger la composition et l'héritage en ayant des supertypes de FlyingBird, FlightlessBirdqui ont le comportement correct injecté par une usine, que les sous-types pertinents Penguin : FlightlessBirdobtiennent, par exemple , automatiquement, et tout autre élément vraiment spécifique est géré par l'usine automatiquement.

StuperUser
la source
1
J'ai mentionné le modèle Décorateur dans ma réponse, mais le modèle Stratégie fonctionne également très bien.
jhocking
1
+1 pour "Privilégier la composition à l'héritage". Cependant, la nécessité de modèles de conception spéciaux pour implémenter la composition dans des langages de type statique renforce mon parti pris pour les langages dynamiques comme Ruby.
Roy Tinker
11

N'est-ce pas le vrai problème que vous supposez Birdavoir une Flyméthode? Pourquoi pas:

class Bird
{
    // features that all birds have
}

class BirdThatCanSwim : Bird
{
    public void Swim() {...};
}

class BirdThatCanFly : Bird
{
    public void Fly() {...};
}


class Penguin : BirdThatCanSwim { }
class Sparrow : BirdThatCanFly { }

Maintenant, le problème évident est l'héritage multiple ( Duck), donc ce dont vous avez vraiment besoin sont des interfaces:

interface IBird { }
interface IBirdThatCanSwim : IBird { public void Swim(); }
interface IBirdThatCanFly : IBird { public void Fly(); }
interface IBirdThatCanQuack : IBird { public void Quack(); }

class Duck : BirdThatCanFly, IBirdThatCanSwim, IBirdThatCanQuack
{
    public void Swim() {...};
    public void Quack() {...};
}
Scott Whitlock
la source
3
Le problème est que l'évolution ne suit pas le principe de substitution de Liskov et hérite avec la suppression des fonctionnalités.
Donal Fellows
7

Tout d'abord, OUI, tout langage qui permet une modification dynamique facile des objets vous permettrait de le faire. Dans Ruby, par exemple, vous pouvez facilement supprimer une méthode.

Mais comme l'a dit Péter Török, cela violerait le LSP .


Dans cette partie, j'oublierai le LSP et supposer que:

  • Bird est une classe avec une méthode fly ()
  • Le pingouin doit hériter de Bird
  • Le pingouin ne peut pas voler ()
  • Je me fiche que ce soit un bon design ou qu'il corresponde au monde réel, comme c'est l'exemple fourni dans cette question.

Tu as dit :

D'une part, vous ne voulez pas définir pour chaque propriété et comportement un indicateur qui spécifie s'il est présent, et le vérifier à chaque fois avant d'accéder à ce comportement ou à cette propriété

On dirait que ce que vous voulez est " demander pardon à Python plutôt que permission " de Python

Faites simplement que votre pingouin lève une exception ou hérite d'une classe NonFlyingBird qui lève une exception (pseudo-code):

class Penguin extends Bird {
     function fly():void {
          throw new Exception("Hey, I'm a penguin, I can't fly !");
     }
}

Au fait, quoi que vous choisissiez: lever une exception ou supprimer une méthode, au final, le code suivant (en supposant que votre langue supporte la suppression de méthode):

var bird:Bird = new Penguin();
bird.fly();

générera une exception d'exécution.

David
la source
"Faites juste que votre pingouin lève une exception ou hérite d'une classe NonFlyingBird qui lève une exception" C'est toujours une violation de LSP. Il suggère toujours qu'un pingouin peut voler, même si sa mise en œuvre de fly doit échouer. Il ne devrait jamais y avoir de méthode de mouche sur Penguin.
pdr
@pdr: cela ne signifie pas qu'un pingouin peut voler, mais qu'il devrait voler (c'est un contrat). L'exception vous dira que ce n'est pas possible . Soit dit en passant, je ne prétends pas que c'est une bonne pratique de POO, je donne juste une réponse à une partie de la question
David
Le fait est qu'un pingouin ne devrait pas voler juste parce qu'il est un oiseau. Si je veux écrire du code qui dit "Si x peut voler, faites ceci; sinon faites cela." Je dois utiliser un try / catch dans votre version, où je devrais simplement pouvoir demander à l'objet s'il peut voler (la méthode de casting ou de vérification existe). Cela peut être juste dans le libellé, mais votre réponse implique que lever une exception est compatible avec LSP.
pdr
@pdr "Je dois utiliser un try / catch dans votre version" -> c'est tout l'intérêt de demander pardon plutôt que permission (car même un canard aurait pu se casser les ailes et ne pas pouvoir voler). Je vais corriger le libellé.
David
"c'est tout l'intérêt de demander pardon plutôt que permission." Oui, sauf qu'il permet au framework de lever le même type d'exception pour toute méthode manquante, donc "try: except AttributeError:" de Python est exactement équivalent à C # "if (X est Y) {} else {}" et instantanément reconnaissable En tant que tel. Mais si vous avez délibérément levé une CannotFlyException pour remplacer la fonctionnalité fly () par défaut dans Bird, elle devient alors moins reconnaissable.
pdr
7

Comme quelqu'un l'a souligné ci-dessus dans les commentaires, les pingouins sont des oiseaux, les pingouins ne volent pas, ergo tous les oiseaux ne peuvent pas voler.

Donc Bird.fly () ne devrait pas exister ou être autorisé à ne pas fonctionner. Je préfère le premier.

Avoir FlyingBird étend Bird avec une méthode .fly () serait bien sûr correcte.

alex
la source
Je suis d'accord, Fly devrait être une interface que Bird peut implémenter. Il pourrait également être implémenté comme une méthode avec un comportement par défaut qui peut être annulé, mais une approche plus propre utilise une interface.
Jon Raynor
6

Le vrai problème avec l'exemple fly () est que l'entrée et la sortie de l'opération ne sont pas correctement définies. Que faut-il pour qu'un oiseau vole? Et que se passe-t-il une fois le vol réussi? Les types de paramètres et les types de retour pour la fonction fly () doivent avoir ces informations. Sinon, votre conception dépend d'effets secondaires aléatoires et tout peut arriver. La partie n'importe quoi est à l'origine de tout le problème, l'interface n'est pas correctement définie et toutes sortes d'implémentations sont autorisées.

Donc, au lieu de cela:

class Bird {
public:
   virtual void fly()=0;
};

Vous devriez avoir quelque chose comme ça:

   class Bird {
   public:
      virtual float fly(float x) const=0;
   };

Maintenant, il définit explicitement les limites de la fonctionnalité - votre comportement de vol n'a qu'un seul flotteur à décider - la distance du sol, une fois la position donnée. Maintenant, tout le problème se résout automatiquement. Un oiseau qui ne peut pas voler ne renvoie que 0,0 de cette fonction, il ne quitte jamais le sol. C'est un comportement correct pour cela, et une fois qu'un flottant est décidé, vous savez que vous avez entièrement implémenté l'interface.

Un comportement réel peut être difficile à coder pour les types, mais c'est la seule façon de spécifier correctement vos interfaces.

Edit: je veux clarifier un aspect. Cette version float-> float de la fonction fly () est également importante car elle définit un chemin. Cette version signifie qu'un seul oiseau ne peut pas se reproduire comme par magie pendant qu'il vole. C'est pourquoi le paramètre est flottant simple - c'est la position dans le chemin que prend l'oiseau. Si vous voulez des chemins plus complexes, alors Point2d posinpath (float x); qui utilise le même x que la fonction fly ().

tp1
la source
1
J'aime bien votre réponse. Je pense que cela mérite plus de votes.
Sebastien Diot
2
Excellente réponse. Le problème est que la question fait simplement signe de la main de ce que fait fly (). Toute implémentation réelle de fly aurait, à tout le moins, une destination - fly (destination de coordonnées) qui, dans le cas du pingouin, pourrait être outrepassée pour implémenter {return currentPosition)}
Chris Cudmore
4

Techniquement, vous pouvez le faire dans à peu près n'importe quel langage dynamique / typé canard (JavaScript, Ruby, Lua, etc.) mais c'est presque toujours une très mauvaise idée. Supprimer des méthodes d'une classe est un cauchemar de maintenance, semblable à l'utilisation de variables globales (c'est-à-dire que vous ne pouvez pas dire dans un module que l'état global n'a pas été modifié ailleurs).

Les bons modèles pour le problème que vous avez décrit sont Decorator ou Strategy, la conception d'une architecture de composants. Fondamentalement, plutôt que de supprimer les comportements inutiles des sous-classes, vous créez des objets en ajoutant les comportements nécessaires. Donc, pour construire la plupart des oiseaux, vous devez ajouter le composant volant, mais n'ajoutez pas ce composant à vos pingouins.

jhocking
la source
3

Peter a mentionné le principe de substitution de Liskov, mais je pense que cela doit être expliqué.

Soit q (x) une propriété prouvable sur les objets x de type T. Alors q (y) devrait être prouvable pour les objets y de type S où S est un sous-type de T.

Ainsi, si un Oiseau (objet x de type T) peut voler (q (x)) alors un Pingouin (objet y de type S) peut voler (q (y)), par définition. Mais ce n'est clairement pas le cas. Il existe également d'autres créatures qui peuvent voler mais qui ne sont pas de type Oiseau.

La façon dont vous gérez cela dépend de la langue. Si un langage prend en charge l'héritage multiple, vous devez utiliser une classe abstraite pour les créatures qui peuvent voler; si une langue préfère les interfaces, c'est la solution (et l'implémentation de fly devrait être encapsulée plutôt qu'héritée); ou, si un langage prend en charge Duck Typing (sans jeu de mots), vous pouvez simplement implémenter une méthode fly sur les classes qui le peuvent et l'appeler si elle existe.

Mais chaque propriété d'une superclasse doit s'appliquer à toutes ses sous-classes.

[En réponse à modifier]

Appliquer un "trait" de CanFly à Bird n'est pas mieux. Il suggère toujours d'appeler le code que tous les oiseaux peuvent voler.

Un trait dans les termes que vous avez définis, c'est exactement ce que Liskov voulait dire quand elle a dit «propriété».

pdr
la source
2

Permettez-moi de commencer par mentionner (comme tout le monde) le principe de substitution de Liskov, qui explique pourquoi vous ne devriez pas faire cela. Cependant, la question de ce que vous devez faire est celle du design. Dans certains cas, il peut ne pas être important que Penguin ne puisse pas réellement voler. Peut-être que Penguin peut lancer InsufficientWingsException lorsqu'on lui demande de voler, tant que vous êtes clair dans la documentation de Bird :: fly () qu'il peut lancer cela pour les oiseaux qui ne peuvent pas voler. D'avoir un test pour voir s'il peut vraiment voler, bien que cela gonfle l'interface.

L'alternative est de restructurer vos classes. Créons la classe "FlyingCreature" (ou mieux une interface, si vous traitez avec le langage qui le permet). "Bird" n'hérite pas de FlyingCreature, mais vous pouvez créer "FlyingBird" qui le fait. Alouette, Vautour et Aigle héritent tous de FlyingBird. Pingouin ne le fait pas. Il hérite juste de Bird.

C'est un peu plus compliqué que la structure naïve, mais ça a l'avantage d'être précis. Vous noterez que toutes les classes attendues sont là (Bird) et l'utilisateur peut généralement ignorer celles «inventées» (FlyingCreature) s'il n'est pas important de savoir si votre créature peut voler ou non.

DJClayworth
la source
0

La manière typique de gérer une telle situation est de lancer quelque chose comme un UnsupportedOperationException(Java) resp. NotImplementedException(C #).

user281377
la source
Tant que vous documentez cette possibilité dans Bird.
DJClayworth
0

Beaucoup de bonnes réponses avec de nombreux commentaires, mais ils ne sont pas tous d'accord, et je ne peux en choisir qu'un seul, je vais donc résumer ici tous les points de vue avec lesquels je suis d'accord.

0) Ne supposez pas le "typage statique" (je l'ai fait quand j'ai demandé, car je fais Java presque exclusivement). Fondamentalement, le problème dépend beaucoup du type de langage utilisé.

1) Il faut séparer la hiérarchie de types de la hiérarchie de réutilisation de code dans la conception et dans la tête, même si elles se chevauchent le plus souvent. En règle générale, utilisez des classes pour la réutilisation et des interfaces pour les types.

2) La raison pour laquelle normalement Bird IS-A Fly est parce que la plupart des oiseaux peuvent voler, c'est donc pratique du point de vue de la réutilisation du code, mais dire que Bird IS-A Fly est en fait faux car il y a au moins une exception (Manchot).

3) Dans les langages statiques et dynamiques, vous pouvez simplement lever une exception. Mais cela ne doit être utilisé que s'il est explicitement déclaré dans le "contrat" ​​de la classe / interface déclarant la fonctionnalité, sinon c'est une "rupture de contrat". Cela signifie également que vous devez maintenant être prêt à intercepter l'exception partout, donc vous écrivez plus de code sur le site d'appel, et c'est du code laid.

4) Dans certains langages dynamiques, il est en fait possible de "supprimer / masquer" la fonctionnalité d'une super-classe. Si la vérification de la présence de la fonctionnalité est la façon dont vous recherchez "IS-A" dans cette langue, alors c'est une solution adéquate et sensée. Si d'autre part, l'opération "IS-A" est autre chose qui dit toujours que votre objet "devrait" implémenter la fonctionnalité maintenant manquante, alors votre code appelant va supposer que la fonctionnalité est présente et l'appeler et planter, donc cela revient en quelque sorte à lever une exception.

5) La meilleure alternative est de séparer réellement le trait Fly du trait Bird. Ainsi, un oiseau volant doit explicitement étendre / implémenter à la fois Bird et Fly / Flying. C'est probablement la conception la plus propre, car vous n'avez rien à "supprimer". Le seul inconvénient est maintenant que presque chaque oiseau doit implémenter à la fois Bird et Fly, vous écrivez donc plus de code. Le moyen de contourner cela est d'avoir une classe intermédiaire FlyingBird, qui implémente à la fois Bird et Fly, et représente le cas commun, mais cette solution de contournement pourrait être d'une utilité limitée sans héritage multiple.

6) Une autre alternative qui ne nécessite pas d'héritage multiple est d'utiliser la composition au lieu des héritages. Chaque aspect d'un animal est modélisé par une classe indépendante, et un oiseau concret est une composition d'oiseau, et peut-être de voler ou de nager, ... Vous obtenez une réutilisation complète du code, mais devez faire une ou plusieurs étapes supplémentaires pour arriver à la fonctionnalité Vol, lorsque vous avez une référence d'un Oiseau concret. De plus, le langage naturel "objet IS-A Fly" et "objet AS-A (cast) Fly" ne fonctionnera plus, vous devez donc inventer votre propre syntaxe (certains langages dynamiques peuvent avoir un moyen de contourner cela). Cela pourrait rendre votre code plus lourd.

7) Définissez votre caractéristique Fly de telle sorte qu'elle offre une sortie claire pour quelque chose qui ne peut pas voler. Fly.getNumberOfWings () pourrait retourner 0. Si Fly.fly (direction, currentPotinion) devait retourner la nouvelle position après le vol, alors Penguin.fly () pourrait simplement retourner la position actuelle sans la changer. Vous pourriez vous retrouver avec du code qui fonctionne techniquement, mais il y a quelques mises en garde. Premièrement, certains codes peuvent ne pas avoir un comportement "ne rien faire" évident. De plus, si quelqu'un appelle x.fly (), il s'attendrait à ce qu'il fasse quelque chose , même si le commentaire dit que fly () est autorisé à ne rien faire . Enfin, le pingouin IS-A Flying retournerait toujours vrai, ce qui pourrait prêter à confusion pour le programmeur.

8) Faites comme 5), mais utilisez la composition pour contourner les cas qui nécessiteraient un héritage multiple. C'est l'option que je préférerais pour un langage statique, car 6) semble plus lourde (et nécessite probablement plus de mémoire car nous avons plus d'objets). Un langage dynamique pourrait rendre 6) moins encombrant, mais je doute que cela devienne moins encombrant que 5).

Sébastien Diot
la source
0

Définissez un comportement par défaut (marquez-le comme virtuel) dans la classe de base et remplacez-le si nécessaire. De cette façon, chaque oiseau peut "voler".

Même les pingouins volent, ils glissent sur la glace à zéro altitude!

Le comportement de vol peut être annulé si nécessaire.

Une autre possibilité est d'avoir une interface Fly. Tous les oiseaux n'implémenteront pas cette interface.

class eagle : bird, IFly
class penguin : bird

Les propriétés ne peuvent pas être supprimées, c'est pourquoi il est important de savoir quelles propriétés sont communes à tous les oiseaux. Je pense que c'est plus un problème de conception pour s'assurer que les propriétés communes sont implémentées au niveau de base.

Jon Raynor
la source
-1

Je pense que le motif que vous recherchez est un bon vieux polymorphisme. Bien que vous puissiez supprimer une interface d'une classe dans certaines langues, ce n'est probablement pas une bonne idée pour les raisons avancées par Péter Török. Dans n'importe quel langage OO, cependant, vous pouvez remplacer une méthode pour changer son comportement, et cela inclut ne rien faire. Pour emprunter votre exemple, vous pouvez fournir une méthode Penguin :: fly () qui effectue l'une des opérations suivantes:

  • rien
  • lève une exception
  • appelle la méthode Penguin :: swim () à la place
  • affirme que le pingouin est sous l'eau (ils "volent" dans l'eau)

Les propriétés peuvent être un peu plus faciles à ajouter et à supprimer si vous planifiez à l'avance. Vous pouvez stocker des propriétés dans une carte / dictionnaire / tableau associatif au lieu d'utiliser des variables d'instance. Vous pouvez utiliser le modèle Factory pour produire des instances standard de telles structures, de sorte qu'un oiseau provenant de BirdFactory commencera toujours avec le même ensemble de propriétés. Le codage des valeurs clés d'Objective-C est un bon exemple de ce genre de chose.

Remarque: La leçon sérieuse des commentaires ci-dessous est que même si la suppression d'un comportement peut fonctionner, ce n'est pas toujours la meilleure solution. Si vous vous trouvez dans l'obligation de le faire de manière significative, vous devriez considérer qu'un signal fort indique que votre graphique d'héritage est défectueux. Il n'est pas toujours possible de refactoriser les classes dont vous héritez, mais quand c'est le cas, c'est souvent la meilleure solution.

En utilisant votre exemple Pingouin, une façon de refactoriser serait de séparer la capacité de vol de la classe Oiseau. Étant donné que tous les oiseaux ne peuvent pas voler, y compris une méthode fly () dans Bird n'était pas appropriée et conduisait directement au type de problème que vous posez. Donc, déplacez la méthode fly () (et peut-être takeoff () et land ()) vers une classe ou une interface Aviator (selon la langue). Cela vous permet de créer une classe FlyingBird qui hérite à la fois de Bird et Aviator (ou hérite de Bird et implémente Aviator). Penguin peut continuer à hériter directement de Bird mais pas d'Aviator, évitant ainsi le problème. Un tel arrangement pourrait également faciliter la création de classes pour d'autres choses volantes: FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect, etc.

Caleb
la source
2
-1 pour avoir même suggéré d'appeler Penguin :: swim (). Cela viole le principe du moindre étonnement et amènera partout les programmeurs de maintenance à maudire votre nom.
DJClayworth
1
@DJClayworth Comme l'exemple était du côté ridicule en premier lieu, le vote en aval pour violation du comportement inféré de fly () et swim () semble un peu trop. Mais si vous voulez vraiment regarder cela sérieusement, je conviens qu'il est plus probable que vous alliez dans l'autre sens et implémentiez swim () en termes de fly (). Les canards nagent en pagayant leurs pieds; les pingouins nagent en battant des ailes.
Caleb
1
Je suis d'accord que la question était idiote, mais le problème est que j'ai vu des gens faire cela dans la vraie vie - utiliser des appels existants qui «ne font vraiment rien» pour implémenter des fonctionnalités rares. Cela bousille vraiment le code et se termine généralement par l'écriture de "if (! (MyBird instanceof Penguin)) fly ();" dans de nombreux endroits, en espérant que personne ne crée une classe d'autruches.
DJClayworth
L'affirmation est encore pire. Si j'ai un tableau d'oiseaux, qui ont tous la méthode fly (), je ne veux pas d'échec d'assertion lorsque j'appelle fly () sur eux.
DJClayworth
1
Je n'ai pas lu la documentation de Penguin , car on m'a remis un tableau d'oiseaux et je ne savais pas qu'un pingouin serait dans le tableau. J'ai lu la documentation Bird qui disait que lorsque j'appelle fly () l'oiseau vole. Si cette documentation avait clairement indiqué qu'une exception pourrait être levée si l'oiseau était incapable de voler, je l'aurais autorisé. S'il disait que l'appel à fly () l'aurait parfois fait nager, j'aurais changé pour utiliser une bibliothèque de classes différente. Ou parti pour une très grande boisson.
DJClayworth