Sollte ich in diesem Szenario Komposition oder Vererbung bevorzugen?

11

Betrachten Sie eine Schnittstelle:

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

Diese Schnittstelle wird von einer Reihe von Klassen implementiert, die Wellen unterschiedlicher Form erzeugen (z. B. SineWaveGeneratorund SquareWaveGenerator).

Ich möchte eine Klasse implementieren, die SoundWavebasierend auf Musikdaten und nicht auf Rohtondaten generiert . Es würde den Namen einer Note und eine Länge in Beats (nicht Sekunden) erhalten und die IWaveGeneratorFunktionalität intern verwenden , um eine SoundWaveentsprechende Note zu erstellen .

Die Frage ist, sollte das eine NoteGeneratorenthalten IWaveGeneratoroder sollte es von einer IWaveGeneratorImplementierung erben ?

Ich neige aus zwei Gründen zur Komposition:

1- Es erlaubt mir , jeden zu injizieren , IWaveGeneratorum die NoteGeneratordynamisch. Auch ich brauche nur eine NoteGeneratorKlasse, statt SineNoteGenerator, SquareNoteGeneratorusw.

2- Es ist nicht erforderlich NoteGenerator, die durch definierte untergeordnete Schnittstelle freizulegen IWaveGenerator.

Allerdings poste ich diese Frage, um andere Meinungen dazu zu hören, vielleicht Punkte, an die ich nicht gedacht habe.

Übrigens: Ich würde sagen, es NoteGenerator ist konzeptionell ein, IWaveGeneratorweil es SoundWaves erzeugt .

Aviv Cohn
quelle

Antworten:

14

Damit kann ich jeden IWaveGenerator dynamisch in den NoteGenerator einfügen. Außerdem benötige ich nur eine NoteGenerator-Klasse anstelle von SineNoteGenerator , SquareNoteGenerator usw.

Das ist ein klares Zeichen, dass es besser wäre, hier Komposition zu verwenden und nicht von SineGeneratoroder SquareGeneratoroder (schlimmer) von beiden zu erben . Trotzdem ist es sinnvoll, einen NoteGenerator direkt von einem zu erben, IWaveGeneratorwenn Sie diesen ein wenig ändern.

Das eigentliche Problem hier ist, dass es wahrscheinlich sinnvoll ist, NoteGeneratormit einer Methode wie zu haben

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

aber nicht mit einer Methode

SoundWave GenerateWave(double frequency, double lengthInSeconds);

weil diese Schnittstelle zu spezifisch ist. Sie möchten, dass IWaveGenerators Objekte sind, die SoundWaves erzeugen , aber derzeit drückt Ihre Schnittstelle aus, dass IWaveGenerators Objekte sind, die SoundWaves ausschließlich aus Häufigkeit und Länge erzeugen . Entwerfen Sie eine solche Schnittstelle besser auf diese Weise

interface IWaveGenerator
{
    SoundWave GenerateWave();
}

und übergeben Sie Parameter wie frequencyoder lengthInSecondsoder einen völlig anderen Parametersatz durch die Konstruktoren von a SineWaveGenerator, a SquareGeneratoroder einem anderen Generator, an den Sie denken. Auf diese Weise können Sie andere Arten von IWaveGenerators mit völlig anderen Konstruktionsparametern erstellen . Vielleicht möchten Sie einen Rechteckwellengenerator hinzufügen, der eine Frequenz und zwei Längenparameter benötigt, oder so ähnlich, vielleicht möchten Sie als nächstes einen Dreieckwellengenerator hinzufügen, ebenfalls mit mindestens drei Parametern. Oder ein NoteGenerator, mit Konstruktorparameter noteName, noOfBeatsund waveGenerator.

Die allgemeine Lösung besteht darin, die Eingabeparameter von der Ausgabefunktion zu entkoppeln und nur die Ausgabefunktion zu einem Teil der Schnittstelle zu machen.

Doc Brown
quelle
Interessant, habe nicht daran gedacht. Aber ich frage mich: Funktioniert dies (Setzen der 'Parameter auf eine polymorphe Funktion' im Konstruktor) in der Realität oft? Denn dann müsste der Code tatsächlich wissen, um welchen Typ es sich handelt, was den Polymorphismus ruiniert. Können Sie ein Beispiel geben, wo dies funktionieren würde?
Aviv Cohn
2
@ AvivCohn: "Der Code müsste tatsächlich wissen, um welchen Typ es sich handelt" - nein, das ist ein Missverständnis. Nur der Teil des Codes, der den spezifischen Generatortyp konstruiert (mybe a factory), und der muss immer wissen, um welchen Typ es sich handelt.
Doc Brown
... und wenn Sie den Konstruktionsprozess Ihrer Objekte polymorph machen müssen, können Sie das Muster "abstrakte Fabrik" ( en.wikipedia.org/wiki/Abstract_factory_pattern ) verwenden
Doc Brown
Dies ist die Lösung, die ich wählen würde. Kleine, unveränderliche Klassen sind der richtige Weg hierher.
Stephen
9

Ob NoteGenerator "konzeptionell" ein IWaveGenerator ist oder nicht, spielt keine Rolle.

Sie sollten nur dann von einer Schnittstelle erben, wenn Sie genau diese Schnittstelle gemäß dem Liskov-Substitutionsprinzip implementieren möchten, dh mit der richtigen Semantik sowie der richtigen Syntax.

Es hört sich so an, als ob Ihr NoteGenerator syntaktisch dieselbe Schnittstelle hat, aber seine Semantik (in diesem Fall die Bedeutung der verwendeten Parameter) ist sehr unterschiedlich, sodass die Verwendung der Vererbung in diesem Fall sehr irreführend und möglicherweise fehleranfällig wäre. Sie bevorzugen hier die Komposition.

Ixrec
quelle
Eigentlich wollte ich nicht NoteGeneratorimplementieren, GenerateWaveaber die Parameter anders interpretieren, ja, ich stimme zu, das wäre eine schreckliche Idee. Ich meinte, NoteGenerator ist eine Art Spezialisierung eines Wellengenerators: Er kann Eingangsdaten mit höherem Pegel anstelle von Rohdaten (z. B. einen Notennamen anstelle einer Frequenz) aufnehmen. Dh sineWaveGenerator.generate(440) == noteGenerator.generate("a4"). Da kommt also die Frage, Zusammensetzung oder Vererbung.
Aviv Cohn
Wenn Sie eine einzige Schnittstelle entwickeln können, die sowohl für die Wellenerzeugungsklassen auf hoher als auch auf niedriger Ebene geeignet ist, kann die Vererbung akzeptabel sein. Aber das scheint sehr schwierig zu sein und es ist unwahrscheinlich, dass es irgendwelche wirklichen Vorteile gibt. Die Komposition scheint definitiv die natürlichere Wahl zu sein.
Ixrec
@Ixrec: Eigentlich ist es nicht sehr schwierig, eine einzige Schnittstelle für alle Arten von Generatoren zu haben. Das OP sollte wahrscheinlich beides tun, die Komposition verwenden, um einen Generator auf niedriger Ebene zu injizieren und von einer vereinfachten Schnittstelle zu erben (aber den NoteGenerator nicht von a erben Low-Level-Generator-Implementierung) Siehe meine Antwort.
Doc Brown
5

2- NoteGenerator muss die von IWaveGenerator definierte untergeordnete Schnittstelle nicht verfügbar machen.

Klingt so, als wäre NoteGeneratores kein WaveGenerator, sollte daher die Schnittstelle nicht implementieren.

Zusammensetzung ist die richtige Wahl.

Eric King
quelle
Ich würde sagen, NoteGenerator ist konzeptionell ein, IWaveGeneratorweil es SoundWaves erzeugt .
Aviv Cohn
1
Nun, wenn es nicht ausgesetzt werden muss GenerateWave, dann ist es kein IWaveGenerator. Aber es klingt wie es verwendet einen IWaveGenerator (vielleicht mehr?), Also Zusammensetzung.
Eric King
@EricKing: Dies ist eine korrekte Antwort, solange man sich an die GenerateWaveFunktion halten muss, wie sie in der Frage geschrieben steht. Aber aus dem obigen Kommentar denke ich, dass das OP nicht wirklich im Sinn hatte.
Doc Brown
3

Sie haben ein solides Argument für die Komposition. Möglicherweise müssen Sie auch die Vererbung hinzufügen. Sie können dies anhand des aufrufenden Codes feststellen. Wenn Sie einen NoteGeneratorin vorhandenen Aufrufcode verwenden möchten, der einen erwartet IWaveGenerator, müssen Sie die Schnittstelle implementieren. Sie suchen nach einem Bedürfnis nach Substituierbarkeit. Ob es konzeptionell ein Wellengenerator ist, ist nebensächlich.

Karl Bielefeldt
quelle
In diesem Fall, dh wenn Sie die Komposition auswählen, diese Vererbung jedoch noch benötigen, um die Substituierbarkeit zu ermöglichen, wird die "Vererbung" beispielsweise benannt IHasWaveGenerator, und die relevante Methode auf dieser Schnittstelle ist die, GetWaveGeneratordie eine Instanz von zurückgibt IWaveGenerator. Natürlich kann die Benennung geändert werden. (Ich versuche nur, mehr Details zu
konkretisieren
2

Es ist in Ordnung NoteGenerator, die Schnittstelle zu implementieren und NoteGeneratoreine interne Implementierung zu haben, die (nach Zusammensetzung) auf eine andere verweist IWaveGenerator.

Im Allgemeinen führt die Komposition zu einem besser wartbaren (dh lesbaren) Code, da Sie keine Komplexität von Überschreibungen haben, über die Sie nachdenken müssen. Ihre Beobachtung über die Klassenmatrix, die Sie bei der Verwendung der Vererbung haben würden, ist ebenfalls zutreffend und kann wahrscheinlich als Codegeruch angesehen werden, der auf die Komposition hinweist.

Vererbung wird besser verwendet, wenn Sie eine Implementierung haben, die Sie spezialisieren oder anpassen möchten, was hier nicht der Fall zu sein scheint: Sie müssen nur die Schnittstelle verwenden.

Erik Eidt
quelle
1
Die NoteGeneratorImplementierung ist nicht in Ordnung , IWaveGeneratorda Noten Beats erfordern. nicht Sekunden-.
Tulains Córdova
Ja, wenn es keine sinnvolle Implementierung der Schnittstelle gibt, sollte die Klasse sie nicht implementieren. Das OP stellte jedoch fest, dass "ich würde sagen, es NoteGeneratorist konzeptionell ein, IWaveGeneratorweil es SoundWaves erzeugt ", und er erwog eine Vererbung, so dass ich mentalen Spielraum für die Möglichkeit nahm, dass es eine Implementierung der Schnittstelle geben könnte, obwohl es eine andere gibt bessere Schnittstelle oder Signatur für die Klasse.
Erik Eidt