Preferisco la composizione o l'ereditarietà in questo scenario?

11

Considera un'interfaccia:

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

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

Voglio implementare una classe che genera un SoundWave basato su dati musicali, non dati grezzi. Riceverà il nome di una nota e una lunghezza in termini di battiti (non secondi) e utilizzerà internamente la funzionalità IWaveGenerator per creare un SoundWave di conseguenza.

La domanda è, se il NoteGenerator contiene un IWaveGenerator o dovrebbe ereditare da un'implementazione IWaveGenerator ?

Sono propenso alla composizione per due motivi:

1- Mi permette di iniettare qualsiasi IWaveGenerator alla NoteGenerator in modo dinamico. Inoltre, ho solo bisogno di una classe NoteGenerator , invece di SineNoteGenerator , SquareNoteGenerator , ecc.

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

Tuttavia sto postando questa domanda per ascoltare altre opinioni riguardo questo, forse punti a cui non ho pensato.

BTW: direi NoteGenerator è concettualmente un IWaveGenerator perché genera SoundWave s.

    
posta Aviv Cohn 19.06.2015 - 01:16
fonte

5 risposte

14

It allows me to inject any IWaveGenerator to the NoteGenerator dynamically. Also, I only need one NoteGenerator class, instead of SineNoteGenerator, SquareNoteGenerator, etc.

Questo è un chiaro segno che sarebbe meglio usare la composizione qui, e non ereditare da SineGenerator o SquareGenerator o (peggio) entrambi. Nondimeno, avrà senso ereditare un NoteGenerator direttamente da un IWaveGenerator se si cambia un po 'il secondo.

Il vero problema qui è, probabilmente è significativo avere NoteGenerator con un metodo come

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 IWaveGenerator s essere oggetti che generano SoundWave s, ma attualmente la tua interfaccia esprime IWaveGenerator s sono oggetti che generano SoundWave s da frequenza e lunghezza esclusivamente . Quindi, è meglio progettare un'interfaccia in questo modo

interface IWaveGenerator
{
    SoundWave GenerateWave();
}

e passa parametri come frequency o lengthInSeconds , o un insieme completamente diverso di parametri attraverso i costruttori di SineWaveGenerator , a SquareGenerator , o qualunque altro generatore tu abbia in mente. Questo ti permetterà di creare un altro tipo di IWaveGenerator s 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, magari vuoi aggiungere un generatore di onde triangolari, anche con almeno tre parametri. Oppure, un NoteGenerator , con parametri di costruzione noteName , noOfBeats e waveGenerator .

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

    
risposta data 19.06.2015 - 12:58
fonte
9

Se NoteGenerator è o meno "concettualmente" un IWaveGenerator non ha importanza.

Dovresti ereditare da un'interfaccia solo se intendi 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 abbia sintatticamente la stessa interfaccia, ma la sua semantica (in questo caso, il significato dei parametri necessari) sarà molto diversa, quindi l'utilizzo dell'ereditarietà in questo caso sarebbe altamente fuorviante e potenzialmente prona. Hai ragione a preferire la composizione qui.

    
risposta data 19.06.2015 - 01:55
fonte
5

2- There's no need for NoteGenerator to expose the lower-level interface defined by IWaveGenerator.

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

La composizione è la scelta corretta.

    
risposta data 19.06.2015 - 01:34
fonte
3

Hai una solida causa per la composizione. Potresti avere un caso per anche aggiungere ereditarietà. Il modo di dire è guardando il codice chiamante. Se vuoi essere in grado di utilizzare un NoteGenerator nel codice chiamante esistente che si aspetta un IWaveGenerator , devi implementare l'interfaccia. Stai cercando un bisogno di sostituibilità. Se concettualmente "è-un" generatore di onde è vicino al punto.

    
risposta data 19.06.2015 - 15:15
fonte
2

Va bene per NoteGenerator implementare l'interfaccia, e inoltre, per NoteGenerator avere un'implementazione interna che fa riferimento (per composizione) a un altro IWaveGenerator .

Generalmente, la composizione risulta in un codice più gestibile (cioè leggibile), poiché non si hanno complessità di sovrascritture su cui ragionare. La tua osservazione sulla matrice delle classi che avresti quando usassi l'ereditarietà è anche al punto, e può probabilmente essere pensata 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.

    
risposta data 19.06.2015 - 01:50
fonte