„Wolę kompozycję niż dziedziczenie” to po prostu dobra heurystyka
Należy wziąć pod uwagę kontekst, ponieważ nie jest to reguła uniwersalna. Nie należy rozumieć, że nigdy nie należy używać dziedziczenia, gdy można tworzyć kompozycje. Gdyby tak było, naprawilibyście to, zakazując dziedziczenia.
Mam nadzieję, że wyjaśnię ten punkt w tym poście.
Nie będę próbował sam bronić zalet kompozycji. Które uważam za nie na temat. Zamiast tego porozmawiam o niektórych sytuacjach, w których programista może rozważyć dziedziczenie, które lepiej byłoby zastosować kompozycję. W tej kwestii dziedziczenie ma swoje zalety, które również uważam za nie na temat.
Przykład pojazdu
Piszę o programistach, którzy próbują robić rzeczy głupio, do celów narracyjnych
Pójdźmy za wariantem klasycznych przykładów, że niektóre kursy OOP używać ... Mamy Vehicle
klasę, a następnie wyprowadzić Car
, Airplane
, Balloon
i Ship
.
Uwaga : jeśli musisz uziemić ten przykład, udawaj, że są to rodzaje obiektów w grze wideo.
Wtedy Car
i Airplane
może mieć jakiś wspólny kod, ponieważ mogą one zarówno toczyć na kole na ziemi. Deweloperzy mogą rozważyć utworzenie w tym celu klasy pośredniej w łańcuchu dziedziczenia. Jednak w rzeczywistości istnieje również wspólny kod między Airplane
i Balloon
. Mogą rozważyć utworzenie innej klasy pośredniej w łańcuchu spadkowym.
Dlatego twórca szukałby wielokrotnego dziedziczenia. W momencie, gdy programiści szukają wielokrotnego dziedziczenia, projekt już się nie udał.
Lepiej jest modelować to zachowanie jako interfejsy i kompozycję, abyśmy mogli go ponownie wykorzystać bez konieczności dziedziczenia wielu klas. Jeśli na przykład programiści utworzą FlyingVehicule
klasę. Mówiliby, że Airplane
jest to FlyingVehicule
(dziedziczenie klas), ale moglibyśmy zamiast tego powiedzieć, że Airplane
ma Flying
składnik (skład) i Airplane
jest IFlyingVehicule
(dziedziczenie interfejsu).
Używając interfejsów, w razie potrzeby możemy mieć wielokrotne dziedziczenie (interfejsów). Ponadto nie łączysz się z konkretną implementacją. Zwiększanie możliwości ponownego użycia i testowania kodu.
Pamiętaj, że dziedziczenie jest narzędziem polimorfizmu. Ponadto polimorfizm jest narzędziem do ponownego użycia. Jeśli możesz zwiększyć możliwość ponownego użycia kodu za pomocą kompozycji, zrób to. Jeśli nie masz pewności, czy jakakolwiek kompozycja zapewnia lepszą przydatność do ponownego użycia, „Preferuj kompozycję zamiast dziedziczenia” jest dobrą heurystyką.
Wszystko to bez wzmianki Amphibious
.
W rzeczywistości możemy nie potrzebować rzeczy, które zejdą z ziemi. Stephen Hurn ma bardziej wymowny przykład w swoich artykułach „Preferuj kompozycję nad spadkiem” część 1 i część 2 .
Substytucyjność i enkapsulacja
Czy powinien A
dziedziczyć czy komponować B
?
Jeśli A
specjalizacja B
tego typu powinna spełniać zasadę podstawienia Liskowa , dziedziczenie jest opłacalne, a nawet pożądane. Jeśli zdarzają się sytuacje, w których A
nie jest to poprawne zastąpienie, B
nie powinniśmy używać dziedziczenia.
Możemy być zainteresowani kompozycją jako formą programowania obronnego do obrony klasy pochodnej . W szczególności, gdy zaczniesz używać B
do innych celów, może pojawić się presja zmiany lub rozszerzenia, aby była bardziej odpowiednia do tych celów. Jeśli istnieje ryzyko, że B
może ujawnić metody, które mogą spowodować nieprawidłowy stan A
, powinniśmy użyć kompozycji zamiast dziedziczenia. Nawet jeśli jesteśmy autorami obu B
i A
martw się o to jedno, dlatego kompozycja ułatwia ponowne użycie B
.
Możemy nawet argumentować, że jeśli istnieją funkcje, B
które A
nie są potrzebne (i nie wiemy, czy te funkcje mogą spowodować nieprawidłowy stan A
, zarówno w obecnej implementacji, jak iw przyszłości), dobrym pomysłem jest użycie kompozycji zamiast dziedziczenia.
Kompozycja ma również tę zaletę, że umożliwia przełączanie implementacji i ułatwia kpiny.
Uwaga : istnieją sytuacje, w których chcemy zastosować kompozycję, mimo że podstawienie jest ważne. Archiwizujemy tę substytucyjność za pomocą interfejsów lub klas abstrakcyjnych (które można wykorzystać, gdy jest innym tematem), a następnie stosujemy kompozycję z zastrzykiem zależności rzeczywistej implementacji.
Wreszcie, oczywiście, istnieje argument, że powinniśmy używać kompozycji do obrony klasy nadrzędnej, ponieważ dziedziczenie przerywa enkapsulację klasy nadrzędnej:
Dziedziczenie naraża podklasę na szczegóły implementacji jej rodzica, często mówi się, że „dziedziczenie przerywa enkapsulację”
- Wzorce projektowe: elementy oprogramowania obiektowego wielokrotnego użytku, Gang of Four
Cóż, to źle zaprojektowana klasa rodzica. Dlatego powinieneś:
Projektuj dziedziczenie lub zabraniaj go.
- Skuteczna Java, Josh Bloch
Problem jo-jo
Innym przypadkiem, w którym pomaga kompozycja, jest problem jo-jo . Oto cytat z Wikipedii:
Podczas opracowywania oprogramowania problem jo-jo to anty-wzorzec, który pojawia się, gdy programista musi czytać i rozumieć program, którego wykres dziedziczenia jest tak długi i skomplikowany, że programista musi ciągle przerzucać między różnymi definicjami klas w celu śledzenia przepływ kontrolny programu.
Możesz rozwiązać na przykład: Twoja klasa C
nie będzie dziedziczyć po klasie B
. Zamiast tego twoja klasa C
będzie miała element typu A
, który może, ale nie musi, być obiektem typu B
. W ten sposób nie będziesz programować w oparciu o szczegóły implementacji B
, ale wbrew umowie, którą A
oferuje interfejs (lub) .
Przykłady liczników
Wiele ram preferuje dziedziczenie nad kompozycją (co jest przeciwieństwem tego, o co argumentowaliśmy). Deweloper może to zrobić, ponieważ włożył dużo pracy w swoją klasę podstawową, ponieważ zaimplementowanie jej z kompozycją zwiększyłoby rozmiar kodu klienta. Czasami wynika to z ograniczeń języka.
Na przykład struktura PHP ORM może tworzyć klasę podstawową, która używa magicznych metod, aby umożliwić pisanie kodu tak, jakby obiekt miał prawdziwe właściwości. Zamiast tego kod obsługiwany przez magiczne metody będzie przechodził do bazy danych, wyszukiwał określone pole (być może buforował je na przyszłe żądanie) i zwrócił. Wykonanie tego z kompozycją wymagałoby od klienta utworzenia właściwości dla każdego pola lub napisania jakiejś wersji kodu metod magicznych.
Dodatek : Istnieją inne sposoby na rozszerzenie obiektów ORM. Dlatego nie sądzę, aby w tej sprawie konieczne było dziedziczenie. To jest tańsze.
W innym przykładzie silnik gier wideo może utworzyć klasę podstawową, która korzysta z kodu natywnego w zależności od platformy docelowej do renderowania 3D i obsługi zdarzeń. Ten kod jest złożony i specyficzny dla platformy. Zajmowanie się tym kodem byłoby bardzo kosztowne i podatne na błędy, ponieważ jest to jeden z powodów korzystania z silnika.
Co więcej, bez części do renderowania 3D, tak działa wiele ram widgetów. Dzięki temu nie musisz się martwić obsługą komunikatów systemu operacyjnego… w wielu językach nie możesz napisać takiego kodu bez rodzimej obsługi. Co więcej, jeśli to zrobisz, zmniejszy to twoją przenośność. Zamiast tego, z dziedziczeniem, pod warunkiem, że programista nie złamie kompatybilności (za dużo); w przyszłości będziesz mógł łatwo przenieść swój kod na dowolne nowe platformy, które obsługują.
Ponadto należy wziąć pod uwagę, że wiele razy chcemy zastąpić tylko kilka metod i pozostawić wszystko inne z domyślnymi implementacjami. Gdybyśmy korzystali z kompozycji, musielibyśmy stworzyć wszystkie te metody, nawet jeśli tylko delegować do zawiniętego obiektu.
W tym argumencie istnieje punkt, w którym kompozycja może być gorsza pod względem łatwości konserwacji niż dziedziczenie (gdy klasa podstawowa jest zbyt złożona). Pamiętaj jednak, że możliwość dziedziczenia dziedziczenia może być gorsza niż w przypadku kompozycji (gdy drzewo dziedziczenia jest zbyt złożone), o czym mówię w przypadku problemu jo-jo.
W prezentowanych przykładach programiści rzadko zamierzają ponownie wykorzystywać kod wygenerowany przez dziedziczenie w innych projektach. Łagodzi to zmniejszoną możliwość ponownego użycia dziedziczenia zamiast kompozycji. Ponadto, korzystając z dziedziczenia, programiści frameworków mogą zapewnić wiele łatwego w użyciu i łatwego do odkrycia kodu.
Końcowe przemyślenia
Jak widać, kompozycja ma pewną przewagę nad dziedziczeniem w niektórych sytuacjach, nie zawsze. Ważne jest, aby wziąć pod uwagę kontekst i różne czynniki (takie jak możliwość ponownego użycia, łatwość konserwacji, testowalność itp.) W celu podjęcia decyzji. Powrót do pierwszego punktu: „Wolę kompozycję niż dziedziczenie” to po prostu dobra heurystyka.
Możesz także zauważyć, że wiele opisywanych przeze mnie sytuacji można w pewnym stopniu rozwiązać za pomocą Cech lub Mixin. Niestety, nie są to wspólne cechy na wspaniałej liście języków i zazwyczaj wiążą się z pewnym kosztem wydajności. Na szczęście ich popularne metody rozszerzeń kuzyna i domyślne implementacje łagodzą niektóre sytuacje.
Mam najnowszy post, w którym mówię o niektórych zaletach interfejsów, dlaczego potrzebujemy interfejsów między interfejsem użytkownika, biznesem i dostępem do danych w języku C # . Pomaga odsprzęgnąć i ułatwia ponowne użycie i testowanie, możesz być zainteresowany.