Dovrei preferire la composizione o l'eredità in questo scenario?


11

Prendi in considerazione un'interfaccia:

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

Questa interfaccia è implementata da un numero di classi che generano ondate di forme diverse (ad esempio, SineWaveGeneratore SquareWaveGenerator).

Voglio implementare una classe che genera una SoundWavebasata su dati musicali, non su dati audio grezzi. Riceverà il nome di una nota e una lunghezza in termini di battute (non secondi) e utilizzerà internamente la IWaveGeneratorfunzionalità per creare di SoundWaveconseguenza.

La domanda è: dovrebbe NoteGeneratorcontenere IWaveGeneratoro dovrebbe ereditare da IWaveGeneratorun'implementazione?

Mi sto orientando verso la composizione per due motivi:

1- Mi permette di iniettare qualsiasi IWaveGeneratoral NoteGeneratormodo dinamico. Inoltre, ho solo bisogno di una NoteGeneratorclasse, invece di SineNoteGenerator, SquareNoteGeneratorecc

2- Non è necessario NoteGeneratoresporre l'interfaccia di livello inferiore definita da IWaveGenerator.

Tuttavia sto pubblicando questa domanda per ascoltare altre opinioni in merito, forse punti a cui non ho pensato.

A proposito: direi che NoteGenerator è concettualmente un IWaveGeneratorperché genera SoundWaves.

Risposte:


14

Mi permette di iniettare dinamicamente qualsiasi IWaveGenerator nel NoteGenerator. Inoltre, ho bisogno solo di una classe NoteGenerator, invece di SineNoteGenerator , SquareNoteGenerator , ecc.

Questo è un chiaro segno che sarebbe stato meglio per la composizione uso qui, e non ereditano da SineGeneratoro SquareGeneratoro (peggio) entrambi. Tuttavia, avrà senso ereditare un NoteGenerator direttamente da un IWaveGeneratorse si modifica un po 'quest'ultimo.

Il vero problema qui è, probabilmente è significativo avere NoteGeneratorcon un metodo simile

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

ma non con un metodo

SoundWave GenerateWave(double frequency, double lengthInSeconds);

perché questa interfaccia è troppo specifica. Vuoi che IWaveGenerators siano oggetti che generano SoundWaves, ma attualmente la tua interfaccia esprime IWaveGenerators sono oggetti che generano SoundWaves esclusivamente in frequenza e lunghezza . Quindi progettare meglio un'interfaccia del genere in questo modo

interface IWaveGenerator
{
    SoundWave GenerateWave();
}

e passa parametri come frequencyo lengthInSeconds, o un insieme completamente diverso di parametri attraverso i costruttori di a SineWaveGenerator, a SquareGeneratoro qualunque altro generatore tu abbia in mente. Ciò ti consentirà di creare altri tipi di IWaveGenerators con parametri di costruzione completamente diversi. Forse vuoi aggiungere un generatore di onde rettangolari che ha bisogno di una frequenza e due parametri di lunghezza, o qualcosa del genere, forse vuoi aggiungere un generatore di onde triangolari, anche con almeno tre parametri. Oppure, una NoteGenerator, con parametri del costruttore noteName, noOfBeatse waveGenerator.

Quindi la soluzione generale qui è di disaccoppiare i parametri di input dalla funzione di output e rendere solo la funzione di output parte dell'interfaccia.


Interessante, non ci ho pensato. Ma mi chiedo: questo (impostare i "parametri su una funzione polimorfica" nel costruttore) spesso funziona nella realtà? Perché allora il codice dovrebbe davvero sapere con quale tipo ha a che fare, rovinando così il polimorfismo. Puoi fare un esempio di come funzionerebbe?
Aviv Cohn,

2
@AvivCohn: "il codice dovrebbe davvero sapere con che tipo ha a che fare" - no, è un'idea sbagliata. Solo la parte del codice che costruisce il tipo specifico di generatore (mybe a factory), e che deve sapere sempre con quale tipo ha a che fare.
Doc Brown,

... e se devi rendere polimorfico il processo di costruzione dei tuoi oggetti, puoi usare il modello "fabbrica astratta" ( en.wikipedia.org/wiki/Abstract_factory_pattern )
Doc Brown,

Questa è la soluzione che sceglierei. Classi piccole e immutabili è la strada giusta per andare qui.
Stephen,

9

Non importa se NoteGenerator sia "concettualmente" un IWaveGenerator.

Dovresti ereditare da un'interfaccia solo se prevedi di implementare quell'interfaccia esatta secondo il Principio di sostituzione di Liskov, cioè con la semantica corretta e la sintassi corretta.

Sembra che il tuo NoteGenerator potrebbe avere sinteticamente la stessa interfaccia, ma la sua semantica (in questo caso, il significato dei parametri che assume) sarà molto diversa, quindi l'uso dell'ereditarietà in questo caso sarebbe altamente fuorviante e potenzialmente soggetto a errori. Hai ragione a preferire la composizione qui.


In realtà non intendevo NoteGeneratorimplementare GenerateWavema interpretare i parametri in modo diverso, sì, sono d'accordo che sarebbe un'idea terribile. Volevo dire che NoteGenerator è una specie di specializzazione di un generatore di onde: è in grado di accettare dati di input di "livello superiore" anziché solo dati audio grezzi (ad esempio un nome di nota anziché una frequenza). Vale a dire sineWaveGenerator.generate(440) == noteGenerator.generate("a4"). Quindi arriva la domanda, la composizione o l'eredità.
Aviv Cohn,

Se riesci a trovare un'unica interfaccia che si adatta sia alle classi di generazione d'onda di livello alto che basso, l'ereditarietà può essere accettabile. Ma questo sembra molto difficile e probabilmente non avrà alcun reale beneficio. La composizione sembra sicuramente la scelta più naturale.
Ixrec,

@Ixrec: in realtà, non è molto difficile avere un'unica interfaccia per tutti i tipi di generatori, l'OP dovrebbe probabilmente fare entrambe le cose, usare la composizione per iniettare un generatore di basso livello ed ereditare da un'interfaccia semplificata (ma non ereditare il NoteGenerator da un implementazione del generatore di basso livello) Vedi la mia risposta.
Doc Brown,

5

2- Non è necessario che NoteGenerator esponga l'interfaccia di livello inferiore definita da IWaveGenerator.

Sembra che NoteGeneratornon sia un WaveGenerator, quindi non dovrebbe implementare l'interfaccia.

La composizione è la scelta corretta.


Direi che NoteGenerator è concettualmente un IWaveGeneratorperché genera SoundWaves.
Aviv Cohn,

1
Bene, se non ha bisogno di esporre GenerateWave, allora non è un IWaveGenerator. Ma sembra che usi un IWaveGenerator (forse di più?), Quindi la composizione.
Eric King,

@EricKing: questa è una risposta corretta purché si debba attenersi alla GenerateWavefunzione così come è scritta nella domanda. Ma dal commento sopra suppongo che non sia quello che l'OP aveva in mente.
Doc Brown,

3

Hai un solido caso di composizione. Potresti avere un caso per aggiungere anche l' eredità. Il modo di dire è guardando il codice chiamante. Se si desidera poter utilizzare un NoteGeneratorcodice di chiamata esistente che prevede un IWaveGenerator, è necessario implementare l'interfaccia. Stai cercando un bisogno di sostituibilità. Che sia concettualmente un generatore di onde "is-a" non ha senso.


In quel caso, cioè scegliendo la composizione, ma necessitando comunque di tale eredità per far avvenire la sostituibilità, la "eredità" verrebbe chiamata ad esempio IHasWaveGenerator, e il metodo pertinente su tale interfaccia sarebbe quello GetWaveGeneratorche restituisce un'istanza di IWaveGenerator. Naturalmente la denominazione può essere cambiata. (Sto solo cercando di dare più dettagli - fammi sapere se i miei dettagli sono sbagliati.)
rwong

2

Va bene NoteGeneratorimplementare l'interfaccia e anche NoteGeneratoravere un'implementazione interna che fa riferimento (per composizione) a un'altra IWaveGenerator.

Generalmente, la composizione si traduce in un codice più gestibile (cioè leggibile), perché non hai complessità di sostituzioni su cui ragionare. Anche la tua osservazione sulla matrice delle classi che avresti quando usi l'ereditarietà è importante e probabilmente può essere considerata come un odore di codice che punta verso la composizione.

L'ereditarietà viene utilizzata meglio quando si dispone di un'implementazione che si desidera specializzare o personalizzare, il che non sembra essere il caso qui: è sufficiente utilizzare l'interfaccia.


1
Non è corretto NoteGeneratorimplementarlo IWaveGeneratorperché le note richiedono ritmi. non secondi-.
Tulains Córdova,

Sì, certamente se non esiste un'implementazione ragionevole dell'interfaccia, la classe non dovrebbe implementarla. Tuttavia, l'OP ha affermato che "Direi che NoteGeneratorconcettualmente è IWaveGeneratorperché genera SoundWaves", e, stava prendendo in considerazione l'ereditarietà, quindi ho preso la latitudine mentale per la possibilità che potesse esserci qualche implementazione dell'interfaccia, anche se ce n'è un'altra migliore interfaccia o firma per la classe.
Erik Eidt,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.