Czy w tym scenariuszu powinienem preferować kompozycję lub dziedziczenie?

11

Rozważ interfejs:

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

Interfejs ten jest implementowany przez wiele klas, które generują fale o różnych kształtach (na przykład SineWaveGeneratori SquareWaveGenerator).

Chcę zaimplementować klasę, która generuje SoundWavedane muzyczne, a nie surowe dane dźwiękowe. Otrzyma nazwę nuty i długość wyrażoną w uderzeniach (nie sekundach) i wewnętrznie użyje tej IWaveGeneratorfunkcji, aby odpowiednio ją utworzyć SoundWave.

Pytanie brzmi: czy powinien NoteGeneratorzawierać IWaveGeneratorlub powinien dziedziczyć z IWaveGeneratorimplementacji?

Skłaniam się do kompozycji z dwóch powodów:

1- Pozwala mi IWaveGeneratorto NoteGeneratordynamicznie wstrzykiwać dowolne . Ponadto, muszę tylko jedną NoteGeneratorklasę, zamiast SineNoteGenerator, SquareNoteGeneratoritp

2- Nie ma potrzeby NoteGeneratorujawniania interfejsu niższego poziomu zdefiniowanego przez IWaveGenerator.

Jednak zamieszczam to pytanie, aby usłyszeć inne opinie na ten temat, być może kwestie, o których nie myślałem.

BTW: Powiedziałbym, że NoteGenerator jest koncepcyjnie, IWaveGeneratorponieważ generuje SoundWaves.

Aviv Cohn
źródło

Odpowiedzi:

14

Pozwala mi dynamicznie wstrzykiwać dowolny IWaveGenerator do NoteGeneratora. Potrzebuję też tylko jednej klasy NoteGenerator, zamiast SineNoteGenerator , SquareNoteGenerator itp.

Jest to wyraźny znak, że byłoby lepiej, aby tu zastosowania kompozycji, a nie dziedziczyć SineGeneratorlub SquareGeneratorlub (co gorsza) oba. Niemniej jednak sensowne będzie dziedziczenie NoteGeneratora bezpośrednio IWaveGeneratorpo jego niewielkiej zmianie.

Prawdziwym problemem tutaj jest to prawdopodobnie znaczenie mają NoteGeneratorze sposobem jak

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

ale nie przy użyciu metody

SoundWave GenerateWave(double frequency, double lengthInSeconds);

ponieważ ten interfejs jest zbyt specyficzny. Chcesz, aby IWaveGenerators były obiektami, które generują SoundWaves, ale obecnie twój interfejs wyraża IWaveGenerators są obiektami, które generują SoundWaves wyłącznie z częstotliwości i długości . Lepiej więc zaprojektuj taki interfejs w ten sposób

interface IWaveGenerator
{
    SoundWave GenerateWave();
}

i przekazuj parametry takie jak frequencylub lengthInSecondszupełnie inny zestaw parametrów przez konstruktory generatora a SineWaveGenerator, a SquareGeneratorlub innego generatora, o którym myślisz. Umożliwi to tworzenie innych rodzajów elementów IWaveGeneratoro zupełnie innych parametrach konstrukcyjnych. Być może chcesz dodać generator fal prostokątnych, który potrzebuje częstotliwości i dwóch parametrów długości, lub coś w tym rodzaju, może chcesz dodać generator fal trójkątnych, również z co najmniej trzema parametrami. Albo, A NoteGenerator, z parametrów konstruktora noteName, noOfBeatsoraz waveGenerator.

Tak więc ogólnym rozwiązaniem jest oddzielenie parametrów wejściowych od funkcji wyjściowej i uczynienie z funkcji wyjściowej tylko części interfejsu.

Doktor Brown
źródło
Ciekawe, nie myślałem o tym. Zastanawiam się jednak: czy to (ustawianie „parametrów funkcji polimorficznej” w konstruktorze) często działa w rzeczywistości? Ponieważ wtedy kod rzeczywiście musiałby wiedzieć, z jakim typem ma do czynienia, niszcząc w ten sposób polimorfizm. Czy możesz podać przykład, w którym to zadziała?
Aviv Cohn,
2
@AvivCohn: „kod rzeczywiście musiałby wiedzieć, z jakim typem ma do czynienia” - nie, to błędne przekonanie. Tylko część kodu, która konstruuje określony typ generatora (mybe a factory) i która zawsze musi wiedzieć, z jakim typem ma do czynienia.
Doc Brown
... a jeśli chcesz, aby proces budowy obiektów był polimorficzny, możesz użyć wzorca „fabryki abstrakcyjnej” ( en.wikipedia.org/wiki/Abstract_factory_pattern )
Doc Brown
To jest rozwiązanie, które wybrałbym. Małe, niezmienne klasy to właściwa droga do tego miejsca.
Stephen
9

Niezależnie od tego, czy NoteGenerator jest „koncepcyjnie”, IWaveGenerator nie ma znaczenia.

Powinieneś dziedziczyć po interfejsie tylko wtedy, gdy planujesz zaimplementować ten dokładnie interfejs zgodnie z zasadą podstawienia Liskowa, tj. Z poprawną semantyką, a także z prawidłową składnią.

Wygląda na to, że NoteGenerator może mieć składniowo ten sam interfejs, ale jego semantyka (w tym przypadku znaczenie parametrów, które przyjmuje) będzie bardzo różna, więc użycie dziedziczenia w tym przypadku byłoby bardzo mylące i potencjalnie podatne na błędy. Masz rację, preferując kompozycję.

Ixrec
źródło
Właściwie nie miałem na myśli, NoteGeneratorże będę implementował, GenerateWaveale interpretuje parametry inaczej, tak, zgadzam się, że byłby to okropny pomysł. Miałem na myśli, że NoteGenerator jest swoistą specjalizacją generatora fal: może pobierać dane wejściowe „wyższego poziomu” zamiast tylko surowych danych dźwiękowych (np. Nazwa nuty zamiast częstotliwości). Tj sineWaveGenerator.generate(440) == noteGenerator.generate("a4"). Pojawia się więc pytanie, skład lub dziedziczenie.
Aviv Cohn,
Jeśli możesz zaproponować jeden interfejs, który pasuje zarówno do klas generowania fal wysokiego, jak i niskiego poziomu, dziedziczenie może być akceptowalne. Ale wydaje się to bardzo trudne i mało prawdopodobne, aby przyniosło jakiekolwiek realne korzyści. Kompozycja zdecydowanie wydaje się bardziej naturalnym wyborem.
Ixrec
@Ixrec: w rzeczywistości nie jest bardzo trudno mieć jeden interfejs dla wszystkich typów generatorów, OP powinien prawdopodobnie zrobić oba, użyć kompozycji, aby wstrzyknąć generator niskiego poziomu i odziedziczyć po uproszczonym interfejsie (ale nie dziedziczyć NoteGenerator z implementacja generatora niskiego poziomu) Zobacz moją odpowiedź.
Doc Brown
5

2- NoteGenerator nie musi ujawniać interfejsu niższego poziomu zdefiniowanego przez IWaveGenerator.

Wygląda na NoteGeneratorto, że nie jest WaveGenerator, więc nie powinien implementować interfejsu.

Kompozycja to właściwy wybór.

Eric King
źródło
Powiedziałbym, że NoteGenerator jest koncepcyjnie, IWaveGeneratorponieważ generuje SoundWaves.
Aviv Cohn
1
Cóż, jeśli nie trzeba tego ujawniać GenerateWave, to nie jest to IWaveGenerator. Ale wygląda na to, że używa IWaveGenerator (może więcej?), Stąd kompozycja.
Eric King,
@EricKing: jest to prawidłowa odpowiedź, o ile trzeba trzymać się GenerateWavefunkcji opisanej w pytaniu. Ale z powyższego komentarza sądzę, że nie o to tak naprawdę chodziło PO.
Doc Brown
3

Masz solidne argumenty za kompozycją. Możesz mieć sprawę również dodać dziedziczenia. Można to powiedzieć, patrząc na kod wywołujący. Jeśli chcesz mieć możliwość użycia NoteGeneratoristniejącego kodu wywołującego, który oczekuje IWaveGenerator, musisz zaimplementować interfejs. Szukasz potrzeby zastępowalności. Czy koncepcyjnie generator fal „is-a” jest poza sednem sprawy.

Karl Bielefeldt
źródło
W takim przypadku, tj. Wybierając kompozycję, ale nadal potrzebując tego dziedziczenia, aby nastąpiło podstawienie, „dziedziczenie” byłoby nazwane np. IHasWaveGeneratorI odpowiednia metoda na tym interfejsie GetWaveGeneratorzwróciłaby instancję IWaveGenerator. Oczywiście nazewnictwo można zmienić. (Próbuję tylko przedstawić więcej szczegółów - daj mi znać, jeśli moje dane są błędne.)
rwong
2

Dobrze jest NoteGeneratorzaimplementować interfejs, a także NoteGeneratormieć wewnętrzną implementację, która odwołuje się (według składu) do innej IWaveGenerator.

Ogólnie kompozycja powoduje, że kod jest łatwiejszy w utrzymaniu (tj. Czytelny), ponieważ nie ma złożoności przesłonięć do uzasadnienia. Twoje spostrzeżenie na temat macierzy klas, które miałbyś podczas korzystania z dziedziczenia, jest również trafne i prawdopodobnie można je uznać za zapach kodu wskazujący na kompozycję.

Dziedziczenie jest lepiej stosowane, gdy masz implementację, którą chcesz specjalizować lub dostosowywać, co nie wydaje się tak w tym przypadku: wystarczy użyć interfejsu.

Erik Eidt
źródło
1
NoteGeneratorWdrożenie nie jest w porządku, IWaveGeneratorponieważ notatki wymagają beatów. nie sekund.
Tulains Córdova
Tak, oczywiście, jeśli nie ma sensownej implementacji interfejsu, klasa nie powinna go implementować. Jednak OP stwierdził, że „Powiedziałbym, że NoteGeneratorjest koncepcyjnie, IWaveGeneratorponieważ generuje SoundWaves”, i rozważał dziedziczenie, więc wziąłem umysłową swobodę za możliwość, że może być jakaś implementacja interfejsu, nawet jeśli jest inna lepszy interfejs lub podpis dla klasy.
Erik Eidt,