Dois-je préférer la composition ou l'héritage dans ce scénario?

11

Considérez une interface:

interface IWaveGenerator
{
    SoundWave GenerateWave(double frequency, double lengthInSeconds);
}

Cette interface est implémentée par un certain nombre de classes qui génèrent des vagues de formes différentes (par exemple, SineWaveGeneratoret SquareWaveGenerator).

Je veux implémenter une classe qui génère un SoundWavebasé sur des données musicales, pas des données sonores brutes. Il recevrait le nom d'une note et une longueur en termes de battements (pas de secondes), et utiliserait en interne la IWaveGeneratorfonctionnalité pour créer un en SoundWaveconséquence.

La question est de savoir si le doit NoteGeneratorcontenir IWaveGeneratorou doit-il hériter d'une IWaveGeneratorimplémentation?

Je penche vers la composition pour deux raisons:

1- Il me permet d'injecter tout IWaveGeneratorle NoteGeneratordynamique. En outre, je ne ai besoin d' une NoteGeneratorclasse, au lieu de SineNoteGenerator, SquareNoteGeneratoretc.

2- Il n'est pas nécessaire NoteGeneratord'exposer l'interface de niveau inférieur définie par IWaveGenerator.

Cependant, je poste cette question pour entendre d'autres opinions à ce sujet, peut-être des points auxquels je n'ai pas pensé.

BTW: Je dirais que NoteGenerator c'est conceptuellement un IWaveGeneratorparce qu'il génère l' SoundWaveart.

Aviv Cohn
la source

Réponses:

14

Cela me permet d'injecter n'importe quel IWaveGenerator au NoteGenerator dynamiquement. De plus, je n'ai besoin que d'une seule classe NoteGenerator, au lieu de SineNoteGenerator , SquareNoteGenerator , etc.

C'est un signe clair qu'il serait préférable d'utiliser la composition ici, et de ne pas hériter des SineGeneratorou SquareGeneratorou (pire) des deux. Néanmoins, il sera logique d'hériter directement d'un NoteGenerator d'un IWaveGeneratorsi vous changez un peu ce dernier.

Le vrai problème ici est qu'il est probablement significatif d'avoir NoteGeneratorune méthode comme

SoundWave GenerateWave(string noteName, double noOfBeats, IWaveGenerator waveGenerator);

mais pas avec une méthode

SoundWave GenerateWave(double frequency, double lengthInSeconds);

car cette interface est trop spécifique. Vous voulez que IWaveGenerators soit des objets qui génèrent SoundWaves, mais actuellement votre interface exprime IWaveGenerators sont des objets qui génèrent SoundWaves uniquement à partir de la fréquence et de la longueur . Donc, mieux concevoir une telle interface de cette façon

interface IWaveGenerator
{
    SoundWave GenerateWave();
}

et passez des paramètres comme frequencyou lengthInSeconds, ou un ensemble de paramètres complètement différent via les constructeurs de a SineWaveGenerator, a SquareGeneratorou tout autre générateur que vous avez en tête. Cela vous permettra de créer d'autres types de IWaveGenerators avec des paramètres de construction complètement différents. Peut-être que vous voulez ajouter un générateur d'onde rectangulaire qui a besoin d'une fréquence et de deux paramètres de longueur, ou quelque chose comme ça, peut-être que vous voulez ajouter un générateur d'onde triangulaire à côté, également avec au moins trois paramètres. Ou, un NoteGenerator, avec des paramètres du constructeur noteName, noOfBeats, et waveGenerator.

La solution générale consiste donc à découpler les paramètres d'entrée de la fonction de sortie et à n'intégrer que la fonction de sortie à l'interface.

Doc Brown
la source
Intéressant, je n'y ai pas pensé. Mais je me demande: est-ce que cela (définir les «paramètres sur une fonction polymorphe» dans le constructeur) fonctionne souvent en réalité? Parce qu'alors le code devrait en effet savoir de quel type il s'agit, ruinant ainsi le polymorphisme. Pouvez-vous donner un exemple où cela fonctionnerait?
Aviv Cohn
2
@AvivCohn: "le code devrait en effet savoir de quel type il s'agit" - non, c'est une idée fausse. Seule la partie du code qui construit le type spécifique de générateur (mybe a factory), et qui doit toujours savoir de quel type il s'agit.
Doc Brown
... et si vous avez besoin de rendre le processus de construction de vos objets polymorphe, vous pouvez utiliser le modèle "usine abstraite" ( en.wikipedia.org/wiki/Abstract_factory_pattern )
Doc Brown
C'est la solution que je choisirais. De petites classes immuables sont la bonne façon d'aller ici.
Stephen
9

Que NoteGenerator soit ou non "conceptuellement" un IWaveGenerator n'a pas d'importance.

Vous ne devez hériter d'une interface que si vous prévoyez d'implémenter cette interface exacte selon le principe de substitution Liskov, c'est-à-dire avec la sémantique correcte ainsi que la syntaxe correcte.

Il semble que votre NoteGenerator puisse avoir syntaxiquement la même interface, mais sa sémantique (dans ce cas, la signification des paramètres qu'il prend) sera très différente, donc l'utilisation de l'héritage dans ce cas serait très trompeuse et potentiellement sujette aux erreurs. Vous avez raison de préférer la composition ici.

Ixrec
la source
En fait, je ne voulais pas NoteGeneratorimplémenter GenerateWavemais interpréter les paramètres différemment, oui je suis d'accord que ce serait une idée terrible. Je voulais dire que NoteGenerator est une sorte de spécialisation d'un générateur d'ondes: il est capable d'accepter des données d'entrée de «niveau supérieur» au lieu de simplement des données sonores brutes (par exemple, un nom de note au lieu d'une fréquence). C'est à dire sineWaveGenerator.generate(440) == noteGenerator.generate("a4"). Alors vient la question, la composition ou l'héritage.
Aviv Cohn
Si vous pouvez proposer une interface unique qui convient à la fois aux classes de génération d'ondes de haut et de bas niveau, l'héritage peut être acceptable. Mais cela semble très difficile et peu susceptible de présenter de réels avantages. La composition semble définitivement le choix le plus naturel.
Ixrec
@Ixrec: en fait, il n'est pas très difficile d'avoir une seule interface pour tous les types de générateurs, l'OP devrait probablement faire les deux, utiliser la composition pour injecter un générateur de bas niveau et hériter d'une interface simplifiée (mais pas hériter le NoteGenerator d'un mise en œuvre d'un générateur de bas niveau) Voir ma réponse.
Doc Brown
5

2- Il n'est pas nécessaire que NoteGenerator expose l'interface de niveau inférieur définie par IWaveGenerator.

Il semble que ce NoteGeneratorne soit pas un WaveGenerator, il ne faut donc pas implémenter l'interface.

La composition est le bon choix.

Eric King
la source
Je dirais que NoteGenerator c'est conceptuellement un IWaveGeneratorparce qu'il génère l' SoundWaveart.
Aviv Cohn
1
Eh bien, s'il n'a pas besoin d'être exposé GenerateWave, ce n'est pas un IWaveGenerator. Mais on dirait qu'il utilise un IWaveGenerator (peut-être plus?), D'où la composition.
Eric King
@EricKing: c'est une bonne réponse tant qu'il faut s'en tenir à la GenerateWavefonction telle qu'elle est écrite dans la question. Mais d'après le commentaire ci-dessus, je suppose que ce n'est pas ce que le PO avait vraiment en tête.
Doc Brown
3

Vous avez un solide dossier de composition. Vous pouvez avoir un cas pour ajouter également l' héritage. La façon de le dire est de regarder le code appelant. Si vous voulez pouvoir utiliser un NoteGeneratordans un code d'appel existant qui attend un IWaveGenerator, vous devez implémenter l'interface. Vous recherchez un besoin de substituabilité. Qu'il soit conceptuellement «générateur de vagues» n'est pas la question.

Karl Bielefeldt
la source
Dans ce cas, c'est-à-dire en choisissant la composition, mais en ayant toujours besoin de cet héritage pour que la substituabilité se produise, l '"héritage" serait nommé par exemple IHasWaveGenerator, et la méthode appropriée sur cette interface serait celle GetWaveGeneratorqui renvoie une instance de IWaveGenerator. Bien sûr, le nom peut être modifié. (J'essaie juste de donner plus de détails - faites-moi savoir si mes détails sont faux.)
rwong
2

C'est bien pour NoteGeneratorimplémenter l'interface, et aussi, pour NoteGeneratoravoir une implémentation interne qui référence (par composition) une autre IWaveGenerator.

Généralement, la composition se traduit par un code plus facile à gérer (c'est-à-dire lisible), car vous n'avez pas de complexités de remplacements à raisonner. Votre observation sur la matrice des classes que vous auriez lors de l'utilisation de l'héritage est également pertinente, et peut probablement être considérée comme une odeur de code pointant vers la composition.

L'héritage est mieux utilisé lorsque vous avez une implémentation que vous souhaitez spécialiser ou personnaliser, ce qui ne semble pas être le cas ici: il vous suffit d'utiliser l'interface.

Erik Eidt
la source
1
Ce n'est pas OK pour l' NoteGeneratorimplémenter IWaveGeneratorparce que les notes nécessitent des battements. pas secondes-.
Tulains Córdova
Oui, certainement s'il n'y a pas d'implémentation sensible de l'interface, alors la classe ne devrait pas l'implémenter. Cependant, le PO a déclaré que "je dirais que NoteGeneratorc'est conceptuellement un IWaveGeneratorparce qu'il génère des SoundWaves", et, il envisageait l'héritage, j'ai donc pris une latitude mentale pour la possibilité qu'il puisse y avoir une certaine implémentation de l'interface, même s'il existe un autre meilleure interface ou signature pour la classe.
Erik Eidt