Preferowanie kompozycji to nie tylko polimorfizm. Chociaż jest to częścią tego, i masz rację, że (przynajmniej w nominalnie typowanych językach) ludzie tak naprawdę mają na myśli „wolą kombinację kompozycji i implementacji interfejsu”. Ale powody, dla których preferuje się kompozycję (w wielu okolicznościach) są głębokie.
Polimorfizm polega na tym, że jedna zachowuje się na wiele sposobów. Tak więc, generyczne / szablony są cechą „polimorficzną”, o ile pozwalają one jednemu kawałkowi kodu zmieniać jego zachowanie w zależności od typu. W rzeczywistości ten typ polimorfizmu jest naprawdę najlepiej zachowany i jest ogólnie określany jako polimorfizm parametryczny, ponieważ zmienność jest definiowana przez parametr.
Wiele języków zapewnia formę polimorfizmu zwaną „przeładowaniem” lub polimorfizm ad hoc, w którym wiele procedur o tej samej nazwie jest definiowanych w sposób ad hoc, a jeden z nich jest wybierany przez język (być może najbardziej specyficzny). Jest to najmniej dobrze zachowany rodzaj polimorfizmu, ponieważ nic nie łączy zachowania dwóch procedur oprócz rozwiniętej konwencji.
Trzecim rodzajem polimorfizmu jest polimorfizm podtypu . Tutaj procedura zdefiniowana dla danego typu może również działać na całej rodzinie „podtypów” tego typu. Kiedy implementujesz interfejs lub rozszerzasz klasę, ogólnie deklarujesz zamiar utworzenia podtypu. Prawdziwe podtypy podlegają zasadzie substytucji Liskowa, który mówi, że jeśli możesz udowodnić coś o wszystkich obiektach w nadtypie, możesz to udowodnić we wszystkich instancjach w podtypie. Życie staje się niebezpieczne, ponieważ w językach takich jak C ++ i Java ludzie na ogół nie wymuszają, a często nieudokumentowane założenia dotyczące klas, które mogą, ale nie muszą, być prawdziwe w odniesieniu do ich podklas. Oznacza to, że kod jest pisany tak, jakby więcej można było udowodnić, niż jest w rzeczywistości, co powoduje cały szereg problemów, gdy podtyp jest niedbale.
Dziedziczenie jest w rzeczywistości niezależne od polimorfizmu. Biorąc pod uwagę coś „T”, które ma odniesienie do siebie, dziedziczenie następuje, gdy tworzysz nową rzecz „S” z „T”, zastępując odniesienie „T” do siebie odwołaniem do „S”. Ta definicja jest celowo niejasna, ponieważ dziedziczenie może się zdarzyć w wielu sytuacjach, ale najczęstszym jest podklasowanie obiektu, które powoduje zastąpienie this
wskaźnika wywoływanego przez funkcje wirtualne this
wskaźnikiem do podtypu.
Dziedziczenie jest niebezpieczne, podobnie jak wszystkie bardzo potężne rzeczy, które mogą powodować spustoszenie. Załóżmy na przykład, że zastępujesz metodę podczas dziedziczenia po pewnej klasie: wszystko jest w porządku i dobrze, dopóki inna metoda tej klasy nie przyjmie, że dziedziczona metoda zachowuje się w określony sposób, po tym wszystkim, jak zaprojektował ją autor oryginalnej klasy . Możesz częściowo zabezpieczyć się przed tym, deklarując, że wszystkie metody wywoływane przez inną z metod są prywatne lub nie-wirtualne (końcowe), chyba że są one przeznaczone do zastąpienia. Nawet to nie zawsze jest wystarczająco dobre. Czasami możesz zobaczyć coś takiego (w pseudo Javie, mam nadzieję, że czytelne dla użytkowników C ++ i C #)
interface UsefulThingsInterface {
void doThings();
void doMoreThings();
}
...
class WayOfDoingUsefulThings implements UsefulThingsInterface{
private foo stuff;
public final int getStuff();
void doThings(){
//modifies stuff, such that ...
...
}
...
void doMoreThings(){
//ignores stuff
...
}
}
myślisz, że to jest piękne i masz swój własny sposób robienia „rzeczy”, ale używasz dziedziczenia, aby nabyć umiejętność robienia „więcej rzeczy”,
class MyUsefulThings extends WayOfDoingUsefulThings{
void doThings {
//my way
}
}
I wszystko jest dobrze i dobrze. WayOfDoingUsefulThings
został zaprojektowany w taki sposób, że zastąpienie jednej metody nie zmienia semantyki żadnej innej ... z wyjątkiem czekania, nie, nie było. Wygląda na to, że tak było, ale doThings
zmienił się stan zmienny, który miał znaczenie. Tak więc, mimo że nie wywołało żadnych funkcji, które można zastąpić,
void dealWithStuff(WayOfDoingUsefulThings bar){
bar.doThings()
use(bar.getStuff());
}
teraz robi coś innego niż oczekiwano, gdy go zdasz MyUsefulThings
. Co gorsza, możesz nawet nie wiedzieć, że WayOfDoingUsefulThings
składali te obietnice. Może dealWithStuff
pochodzi z tej samej biblioteki WayOfDoingUsefulThings
i getStuff()
nie jest nawet eksportowany przez bibliotekę (pomyśl o klasach przyjaciół w C ++). Co gorsza, zostały pokonane statycznych kontrole języka bez uświadomienia sobie: dealWithStuff
wziął WayOfDoingUsefulThings
tylko upewnić się, że miałoby to getStuff()
funkcja, która zachowywała się w określony sposób.
Używanie kompozycji
class MyUsefulThings implements UsefulThingsInterface{
private way = new WayOfDoingUsefulThings()
void doThings() {
//my way
}
void doMoreThings() {
this.way.doMoreThings();
}
}
przywraca bezpieczeństwo typu statycznego. Ogólnie kompozycja jest łatwiejsza w użyciu i bezpieczniejsza niż dziedziczenie podczas implementacji podtypów. Pozwala również na przesłonięcie ostatecznych metod, co oznacza, że powinieneś swobodnie deklarować wszystko jako ostateczne / nie-wirtualne, z wyjątkiem interfejsów przez zdecydowaną większość czasu.
W lepszym świecie języki automatycznie wstawiałyby płytę główną ze delegation
słowem kluczowym. Większość nie, więc wadą są większe klasy. Chociaż możesz poprosić IDE o napisanie dla ciebie instancji delegującej.
W życiu nie chodzi tylko o polimorfizm. Nie musisz cały czas podtypować. Celem polimorfizmu jest ponowne użycie kodu, ale nie jest to jedyny sposób na osiągnięcie tego celu. Często sensowne jest użycie kompozycji, bez polimorfizmu podtypów, jako sposobu zarządzania funkcjonalnością.
Dziedziczenie behawioralne ma również swoje zastosowania. Jest to jeden z najpotężniejszych pomysłów w informatyce. Wystarczy, że przez większość czasu dobre aplikacje OOP można pisać tylko przy użyciu dziedziczenia interfejsu i kompozycji. Dwie zasady
- Zakaz dziedziczenia lub projektowania
- Wolę kompozycję
są dobrym przewodnikiem z powyższych powodów i nie ponoszą żadnych znacznych kosztów.