Być może właśnie zauważyłeś ludzi, którzy to mówili w ciągu ostatnich kilku miesięcy, ale dobrzy programiści znali to znacznie dłużej. Z pewnością mówiłem to w stosownych przypadkach przez około dekadę.
Chodzi o to, że dziedziczenie wiąże się z dużym pojęciowym kosztem. Gdy używasz dziedziczenia, każde wywołanie metody ma w sobie niejawne przesłanie. Jeśli masz drzewa o głębokim dziedzictwie, wielokrotną wysyłkę lub (co gorsza) jedno i drugie, to zastanawianie się, dokąd konkretna metoda zostanie wysłana w danym wezwaniu, może stać się królewską PITA. To sprawia, że poprawne rozumowanie na temat kodu jest bardziej złożone i utrudnia debugowanie.
Dam prosty przykład do zilustrowania. Załóżmy, że głęboko w drzewie spadkowym ktoś nazwał metodę foo
. Potem pojawia się ktoś inny i dodaje foo
na szczycie drzewa, ale robi coś innego. (Ten przypadek jest bardziej powszechny w przypadku wielokrotnego dziedziczenia.) Teraz osoba pracująca w klasie root złamała niejasną klasę potomną i prawdopodobnie nie zdaje sobie z tego sprawy. Możesz mieć 100% pokrycia testami jednostkowymi i nie zauważyć tego złamania, ponieważ osoba na szczycie nie pomyślałaby o przetestowaniu klasy podrzędnej, a testy dla klasy podrzędnej nie pomyślą o przetestowaniu nowych metod utworzonych u góry . (Wprawdzie istnieją sposoby na napisanie testów jednostkowych, które to wychwycą, ale są też przypadki, w których nie można łatwo napisać testów w ten sposób.)
W przeciwieństwie do tego, gdy używasz kompozycji, przy każdym połączeniu jest zwykle wyraźniejsze, do czego wysyłasz połączenie. (OK, jeśli używasz odwrócenia kontroli, na przykład z wstrzykiwaniem zależności, wtedy zastanawianie się, dokąd idzie połączenie, może być również problematyczne. Ale zwykle łatwiej jest to rozgryźć.) Ułatwia to rozumowanie. Jako bonus, kompozycja powoduje oddzielenie metod od siebie. Powyższy przykład nie powinien się tam zdarzyć, ponieważ klasa potomna przeniósłaby się do jakiegoś niejasnego komponentu i nigdy nie ma pytania, czy wywołanie to foo
było przeznaczone dla niejasnego komponentu, czy głównego obiektu.
Teraz masz całkowitą rację, że dziedziczenie i kompozycja to dwa bardzo różne narzędzia, które służą dwóm różnym rodzajom rzeczy. Jasne, że dziedziczenie niesie ze sobą koncepcyjne koszty, ale gdy jest to właściwe narzędzie do pracy, niesie ze sobą mniej pojęć koncepcyjnych niż próba nieużywania go i robienia ręcznie tego, co robi dla ciebie. Nikt, kto wie, co robią, nie powiedziałby, że nigdy nie powinieneś używać dziedziczenia. Ale upewnij się, że to jest właściwe.
Niestety, wielu programistów dowiaduje się o oprogramowaniu obiektowym, uczy się dziedziczenia, a następnie jak najczęściej używa swojego nowego topora. Co oznacza, że próbują użyć dziedziczenia, w którym kompozycja była właściwym narzędziem. Mam nadzieję, że nauczą się lepiej z czasem, ale często dzieje się tak dopiero po kilku usuniętych kończynach itp. Mówienie im z przodu, że to zły pomysł, przyspiesza proces uczenia się i zmniejsza liczbę kontuzji.