Je sais que la question a été discutée auparavant , mais il semble toujours sous l'hypothèse que l'héritage est au moins parfois préférable à la composition. J'aimerais remettre en question cette hypothèse dans l'espoir de mieux comprendre.
Ma question est la suivante: Depuis vous pouvez tout réaliser avec la composition d'objet que vous pouvez utiliser avec l'héritage classique et depuis l'héritage classique est très souvent mal utilisé [1] et depuis La composition d'objets vous offre la possibilité de changer le runtime de l'objet délégué, pourquoi utiliseriez-vous (jamais} _ l'héritage classique?
Je peux en quelque sorte comprendre pourquoi vous recommanderiez l'héritage dans certains langages tels que Java et C++ qui n'offrent pas de syntaxe pratique pour la délégation. Dans ces langues, vous pouvez économiser beaucoup de frappe en utilisant l'héritage chaque fois qu'il n'est pas clairement incorrect de le faire. Mais d’autres langages comme Objective C et Ruby offrent à la fois l’héritage classique et une syntaxe très pratique pour la délégation. Le langage de programmation Go est le seul langage qui, à ma connaissance, a décidé que l'héritage classique représente plus de problèmes que sa valeur et qu'il ne prend en charge que la délégation pour la réutilisation de code.
Voici une autre façon d’énoncer ma question: même si vous savez que l’héritage classique n’est pas une erreur pour mettre en œuvre un certain modèle, cette raison est-elle suffisante pour l’utiliser au lieu de la composition?
[1] Beaucoup de gens utilisent l'héritage classique pour obtenir du polymorphisme au lieu de laisser leurs classes implémenter une interface. Le but de l'héritage est la réutilisation du code, pas le polymorphisme. De plus, certaines personnes utilisent l'héritage pour modéliser leur compréhension intuitive d'une relation "est-une" ce qui peut souvent être problématique .
Mise à jour
Je veux juste clarifier ce que je veux dire exactement quand je parle d'héritage:
Je parle de le type d’héritage par lequel une classe hérite d’une classe de base partiellement ou totalement implémentée . Je parle pas d'hériter d'une classe de base purement abstraite, ce qui revient au même que l'implémentation d'une interface, ce pour quoi je ne discute pas.
Mise à jour 2
Je comprends que l'héritage est le seul moyen d'obtenir un polymorphisme en C++. Dans ce cas, la raison pour laquelle vous devez l'utiliser est évidente. Ma question se limite donc à des langages tels que Java ou Ruby, qui offrent des moyens distincts d’atteindre le polymorphisme (interfaces et typage de canard, respectivement).
Si vous déléguez tout ce que vous n'avez pas explicitement remplacé à un autre objet implémentant la même interface (l'objet "de base"), alors vous avez essentiellement l'héritage Greenspunned en plus de la composition, mais (dans la plupart des langues) avec beaucoup plus de verbosité. et passe-partout. L'utilisation de la composition au lieu de l'héritage a pour but de ne déléguer que les comportements que vous souhaitez déléguer.
Si vous souhaitez que l'objet utilise tout le comportement de la classe de base, à moins qu'il ne soit explicitement remplacé, l'héritage est le moyen le plus simple, le moins détaillé et le plus simple de l'exprimer.
Le but de l'héritage est la réutilisation du code, pas le polymorphisme.
Ceci est votre erreur fondamentale. Presque exactement le contraire est vrai. Le but principal de l'héritage (public) est de modéliser les relations entre les classes en question. Le polymorphisme est une grande partie de cela.
Lorsqu'il est utilisé correctement, l'héritage ne consiste pas à réutiliser du code existant. Il s’agit plutôt d’être utilisé par du code existant. Autrement dit, si votre code existant peut fonctionner avec la classe de base existante, lorsque vous dérivez une nouvelle classe à partir de cette classe de base existante, cet autre code peut désormais également fonctionner automatiquement avec votre nouvelle classe dérivée.
Il est possible d'utiliser l'héritage pour la réutilisation de code, mais lorsque/si vous le faites, il devrait normalement s'agir d'un héritage privé et non d'un héritage public. Si la langue que vous utilisez supporte bien la délégation, il y a de bonnes chances que vous ayez rarement beaucoup de raisons d'utiliser l'héritage privé. OTOH, l'héritage privé prend en charge certaines choses que la délégation ne (normalement) pas. En particulier, même si le polymorphisme est une préoccupation résolument secondaire dans ce cas, il peut () rester - c’est-à-dire qu’il est possible de partir de l’héritage privé une classe de base qui est presque ce que vous voulez, et (à supposer que cela le permette) remplace les parties qui ne sont pas tout à fait correctes.
Avec la délégation, votre seul véritable choix consiste à utiliser la classe existante telle quelle. S'il ne fait pas ce que vous voulez, votre seul choix est d'ignorer complètement cette fonctionnalité et de la ré-implémenter à partir de la base. Dans certains cas, ce n'est pas une perte, mais dans d'autres c'est assez substantiel. Si d'autres parties de la classe de base utilisent la fonction polymorphe, l'héritage privé vous permet de remplacer uniquement la fonction polymorphe et les autres parties utiliseront votre fonction remplacée. Avec la délégation, vous ne pouvez pas facilement brancher votre nouvelle fonctionnalité, aussi d'autres parties de la classe de base existante utiliseront-elles ce que vous avez remplacé.
La principale raison d'utiliser l'héritage est pas en tant que forme de composition - c'est pour vous permettre d'obtenir un comportement polymorphe. Si vous n'avez pas besoin de polymorphisme, vous ne devriez probablement pas utiliser l'héritage, du moins en C++.
Tout le monde sait que le polymorphisme est un grand avantage de l'héritage. Un autre avantage que je trouve dans l'héritage est qu'il aide à créer une réplique du monde réel. Par exemple, dans le système de paie, nous traitons les gestionnaires développeurs, les employés de bureau, etc. si nous héritons de toutes ces classes avec la classe super Employee. cela rend notre programme plus compréhensible dans le contexte du monde réel que toutes ces classes sont essentiellement des employés. Et une dernière chose que les classes contiennent non seulement des méthodes, mais également des attributs. Donc, si nous contenons des attributs génériques pour employé dans la classe Employé Tels que l'âge du numéro de sécurité sociale, etc., cela permettrait une plus grande réutilisation du code, une clarté conceptuelle et, bien entendu, un polymorphisme. Cependant, lors de l’utilisation des éléments d’héritage, nous devons garder à l’esprit le principe de conception de base "Identifiez les aspects de votre application qui varient et séparez-les de ceux qui changent". Vous ne devriez jamais implémenter ces aspects de l'application qui changent d'héritage à l'aide de la composition. Et pour les aspects qui ne sont pas modifiables, vous devez utiliser l'héritage de cours si une relation évidente "est une".
L'héritage est à privilégier si:
Ma conclusion: La délégation est une solution de contournement pour un bogue dans un langage de programmation.
Je réfléchis toujours à deux fois avant d'utiliser l'héritage, car il peut être difficile rapidement. Cela étant dit, il existe de nombreux cas où il produit simplement le code le plus élégant.
Les interfaces ne définissent que ce qu'un objet peut faire et pas comment. Donc, en termes simples, les interfaces ne sont que des contrats. Tous les objets qui implémentent l'interface devront définir leur propre implémentation du contrat. Dans le monde pratique, cela vous donne separation of concern
. Imaginez-vous en train d’écrire une application qui doit traiter divers objets que vous ne connaissez pas à l’avance, mais vous devez tout de même vous en occuper. La seule chose que vous savez est ce que ces objets sont supposés faire. Vous allez donc définir une interface et mentionner toutes les opérations du contrat. Vous allez maintenant écrire votre application sur cette interface. Plus tard, quiconque voudra exploiter votre code ou votre application devra implémenter l'interface sur l'objet pour le faire fonctionner avec votre système. Votre interface forcera leur objet à définir comment chaque opération définie dans le contrat doit être effectuée. De cette manière, tout le monde peut écrire des objets qui implémentent votre interface, afin de les adapter parfaitement à votre système. Tout ce que vous savez, c'est ce qu'il faut faire et c'est l'objet qui doit définir comment cela se fait.
Dans le développement du monde réel, ceci la pratique est généralement connue sous le nom de
Programming to Interface and not to Implementation
.Les interfaces ne sont que des contrats ou des signatures et elles ne savent pas rien sur les implémentations.
Le codage par rapport aux moyens d’interface signifie que le code client contient toujours un objet Interface fourni par une fabrique. Toute instance renvoyée par la fabrique serait du type Interface que toute classe candidate à la fabrique devrait avoir implémentée. De cette façon, le programme client ne s'inquiète pas de la mise en œuvre et la signature de l'interface détermine ce que toutes les opérations peuvent être effectuées. Cela peut être utilisé pour changer le comportement d'un programme au moment de l'exécution. Cela vous aide également à écrire des programmes bien meilleurs du point de vue de la maintenance.
Voici un exemple de base pour vous.
public enum Language
{
English, German, Spanish
}
public class SpeakerFactory
{
public static ISpeaker CreateSpeaker(Language language)
{
switch (language)
{
case Language.English:
return new EnglishSpeaker();
case Language.German:
return new GermanSpeaker();
case Language.Spanish:
return new SpanishSpeaker();
default:
throw new ApplicationException("No speaker can speak such language");
}
}
}
[STAThread]
static void Main()
{
//This is your client code.
ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English);
speaker.Speak();
Console.ReadLine();
}
public interface ISpeaker
{
void Speak();
}
public class EnglishSpeaker : ISpeaker
{
public EnglishSpeaker() { }
#region ISpeaker Members
public void Speak()
{
Console.WriteLine("I speak English.");
}
#endregion
}
public class GermanSpeaker : ISpeaker
{
public GermanSpeaker() { }
#region ISpeaker Members
public void Speak()
{
Console.WriteLine("I speak German.");
}
#endregion
}
public class SpanishSpeaker : ISpeaker
{
public SpanishSpeaker() { }
#region ISpeaker Members
public void Speak()
{
Console.WriteLine("I speak Spanish.");
}
#endregion
}
Qu'en est-il du modèle de méthode template? Supposons que vous ayez une classe de base avec des tonnes de points pour les stratégies personnalisables, mais un modèle de stratégie n'a pas de sens pour au moins l'une des raisons suivantes:
Les stratégies personnalisables doivent connaître la classe de base, ne peuvent être utilisées qu'avec la classe de base et n'ont de sens dans aucun autre contexte. Utiliser une stratégie à la place est faisable, mais un PITA, car la classe de base et la classe de politique doivent avoir des références l'une par rapport à l'autre.
Les politiques sont liées l'une à l'autre en ce sens qu'il ne serait pas logique de les mélanger et de les assortir librement. Ils n'ont de sens que dans un sous-ensemble très limité de toutes les combinaisons possibles.
L'utilité principale de l'héritage classique est si vous avez un certain nombre de classes liées qui auront une logique identique pour les méthodes qui fonctionnent sur des variables/propriétés d'instance.
Il y a vraiment 3 façons de gérer ça:
Maintenant, il peut y avoir une mauvaise utilisation de l'héritage. Par exemple, Java a les classes InputStream
et OutputStream
. Des sous-classes de celles-ci sont utilisées pour lire/écrire des fichiers, des sockets, des tableaux, des chaînes, et plusieurs d'entre elles sont utilisées pour envelopper d'autres flux d'entrée/sortie. Basé sur ce qu'ils font, ceux-ci auraient dû être des interfaces plutôt que des classes.
Tu as écrit:
[1] Beaucoup de gens utilisent le classique héritage pour réaliser le polymorphisme au lieu de laisser leurs cours implémenter une interface. Le but de l'héritage est une réutilisation de code, pas polymorphisme. En outre, certaines personnes utiliser l'héritage pour modéliser leur compréhension intuitive d'un "est-un" relation qui peut souvent être problématique.
Dans la plupart des langues, la ligne de démarcation entre «implémenter une interface» et «dériver une classe d'une autre» est très mince. En fait, dans des langages tels que C++, si vous dérivez une classe B d'une classe A et que A est une classe composée uniquement de méthodes virtuelles pures, vous mettez en œuvre une interface.
L'héritage concerne la réutilisation d'interface, pas la réutilisation d'implémentation. Pas à propos de la réutilisation du code, comme vous l'avez écrit ci-dessus.
Comme vous l'avez fait remarquer à juste titre, l'héritage a pour but de modéliser une relation IS-A (le fait que beaucoup de gens se trompent alors n'a rien à voir avec l'héritage en soi). Vous pouvez également dire 'BEHAVES-LIKE-A'. Cependant, le simple fait que quelque chose ait une relation IS-A avec quelque chose d'autre ne signifie pas qu'il utilise le même code (ou même un code similaire) pour remplir cette relation.
Comparez cet exemple C++ qui implémente différentes manières de générer des données. Deux classes utilisent l'héritage (public) pour pouvoir accéder de manière polymorphe:
struct Output {
virtual bool readyToWrite() const = 0;
virtual void write(const char *data, size_t len) = 0;
};
struct NetworkOutput : public Output {
NetworkOutput(const char *Host, unsigned short port);
bool readyToWrite();
void write(const char *data, size_t len);
};
struct FileOutput : public Output {
FileOutput(const char *fileName);
bool readyToWrite();
void write(const char *data, size_t len);
};
Maintenant, imaginez s'il s'agissait de Java. 'Output' n'était pas une structure, mais une 'interface'. Cela pourrait s'appeler 'Inscriptible'. Au lieu de «sortie publique», vous diriez «implémente Writable». Quelle est la différence en ce qui concerne le design?
Aucun.
Quelque chose de totalement non OOP mais pourtant, composition signifie généralement un cache-miss supplémentaire Cela dépend, mais avoir les données plus proches est un avantage.
En règle générale, je refuse de participer à des combats religieux. Utiliser votre propre jugement et votre style est ce que vous pouvez obtenir de mieux.
Quand vous avez demandé:
Même si vous savez que l'héritage classique n'est pas incorrect pour implémenter un certain modèle, cette raison est-elle suffisante pour l'utiliser au lieu de la composition?
La réponse est non. Si le modèle est incorrect (en utilisant l'héritage), il est erroné d'utiliser quoi qu'il en soit.
Voici quelques problèmes d'héritage que j'ai vus:
L'une des manières les plus utiles d'utiliser l'héritage consiste à utiliser des objets d'interface graphique.