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.
la source
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)" 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?NotSupportedException
dePenguin.fly()
.class Penguin < Bird; undef fly; end;
. Si vous devez ou non est une autre question.Réponses:
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:
Dans ce cas particulier,
Penguin
observe activement laBird.fly
méthode dont il hérite en écrivant unefly
propriété avec une valeurundefined
dans l'objet.Maintenant, vous pouvez dire que
Penguin
cela ne peut plus être traité comme une normaleBird
. Mais comme mentionné, dans le monde réel, cela ne peut tout simplement pas. Parce que nous modélisonsBird
comme é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
Bird
abstraction 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:
Si vous êtes curieux, j'ai une implémentation de
Object.make
Une addition:
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.
la source
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.
la source
Class
est une sous-classe deModule
même siClass
IS-NOT-AModule
. Mais il est toujours logique d'être une sous-classe, car il réutilise une grande partie du code. OTOH,StringIO
IS-AIO
, mais les deux n'ont pas de relation d'héritage (à part l'évidence des deux héritant deObject
, 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.IO
etStringIO
ont le même protocole, donc le même type, mais leurs classes ne sont pas liées.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
,FlightlessBird
qui ont le comportement correct injecté par une usine, que les sous-types pertinentsPenguin : FlightlessBird
obtiennent, par exemple , automatiquement, et tout autre élément vraiment spécifique est géré par l'usine automatiquement.la source
N'est-ce pas le vrai problème que vous supposez
Bird
avoir uneFly
méthode? Pourquoi pas:Maintenant, le problème évident est l'héritage multiple (
Duck
), donc ce dont vous avez vraiment besoin sont des interfaces:la source
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:
Tu as dit :
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):
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):
générera une exception d'exécution.
la source
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.
la source
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:
Vous devriez avoir quelque chose comme ça:
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 ().
la source
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.
la source
Peter a mentionné le principe de substitution de Liskov, mais je pense que cela doit être expliqué.
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é».
la source
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.
la source
La manière typique de gérer une telle situation est de lancer quelque chose comme un
UnsupportedOperationException
(Java) resp.NotImplementedException
(C #).la source
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).
la source
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.
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.
la source
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:
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.
la source