Podczas nauki programowania obiektowego, po opanowaniu podstaw obiektów, pól i metod, większość czasu spędza się na dziedziczeniu. Dziedziczenie oznacza, że przejmujemy część implementacji od klasy bazowej. Wystarczy utworzyć podklasę klasy bazowej, aby odziedziczyć każde nieprywatne pole i metodę.
Samochód i samolot są pojazdami, więc oczywiste jest, że obie te klasy powinny zostać rozszerzone ze wspólnej klasy bazowej o nazwie Vehicle. Jest to typowy przykład akademicki, ale decydując się na powiązanie tych klas relacją dziedziczenia, powinniśmy być świadomi pewnych konsekwencji.
Rys. 1 Implementacja relacji dziedziczenia.
W tym przypadku klasy są ze sobą ściśle powiązane - oznacza to, że zmiany w zachowaniu każdej klasy można osiągnąć poprzez wprowadzenie zmian w klasie bazowej kod. Może to być zarówno zaletą, jak i wadą - zależy to od tego, jakiego rodzaju zachowania oczekujemy. Jeśli dziedziczenie zostanie zastosowane w niewłaściwym momencie, proces dodawania nowej funkcji może napotkać pewne trudności w implementacji, ponieważ nie będzie ona pasować do utworzonego modelu klasy. Będziemy musieli wybierać między duplikowaniem kodu a reorganizacją naszego modelu - a to może być naprawdę czasochłonny proces. Kod wykonujący relację dziedziczenia możemy nazwać "otwartym-zamkniętym" - oznacza to, że jest on otwarty na rozszerzenia, ale zamknięty na modyfikacje. Zakładając, że w klasie Vehicle istnieje ogólna, zdefiniowana obsługa silnika każdego pojazdu, w momencie, w którym chcielibyśmy dodać do naszej hierarchii klas model pojazdu bez silnika (np. roweru), musielibyśmy dokonać poważnych zmian w naszych klasach.
klasa Pojazd
def start_engine
end
def stop_engine
end
end
class Samolot < Pojazd
def move
start_engine
...
stop_engine
koniec
end
Skład
Jeśli interesuje nas tylko część zachowania istniejącej klasy, dobrą alternatywą dla dziedziczenia jest użycie kompozycji. Zamiast tworzyć podklasy, które dziedziczą każde zachowanie (te, których potrzebujemy i te, których w ogóle nie potrzebujemy), możemy wyizolować potrzebne nam funkcje i wyposażyć nasze obiekty w referencje do nich. W ten sposób porzucamy myślenie, że obiekt jest rodzaj obiekt bazowy, na rzecz stwierdzenia, że zawiera tylko niektóre z jego właściwości.
Rys. 2 Korzystanie z kompozycji
Zgodnie z tym podejściem możemy odizolować kod odpowiedzialny za działanie silnika do autonomicznej klasy o nazwie Engine i umieścić odniesienia do niej tylko w klasach reprezentujących pojazdy z silnikami. Odizolowanie funkcji za pomocą kompozycji uprości strukturę klasy Vehicle i wzmocni hermetyzację poszczególnych klas. Teraz jedynym sposobem, w jaki pojazdy mogą mieć wpływ na silnik, jest użycie jego publicznego interfejsu, ponieważ nie będą już miały informacji o jego implementacji. Co więcej, pozwoli to na użycie różnych typów silników w różnych pojazdach, a nawet umożliwi ich wymianę podczas działania programu. Oczywiście wykorzystanie kompozycji nie jest pozbawione wad - tworzymy luźno powiązany zestaw klas, który można łatwo rozszerzać i który jest otwarty na modyfikacje. Jednocześnie jednak każda klasa jest powiązana z wieloma innymi i musi posiadać informacje o ich interfejsach.
klasa Pojazd
koniec
class Silnik
def start
koniec
def stop
koniec
koniec
class Samolot < Pojazd
def initialize
@engine = Engine.new
end
def move
@engine.start
@engine.stop
end
def change_engine(new_engine)
@engine = new_engine
end
end
Wybór
Oba opisane podejścia mają wady i zalety, jak więc wybrać pomiędzy nimi? Dziedziczenie jest specjalizacją, więc najlepiej stosować je tylko do problemów, w których występują relacje typu "is-a" - mamy więc do czynienia z prawdziwą hierarchią typów. Ponieważ dziedziczenie ściśle wiąże ze sobą klasy, w pierwszej kolejności powinniśmy zawsze rozważyć, czy użyć kompozycji, czy nie. Kompozycja powinna być stosowana w przypadku problemów, w których istnieją relacje typu "has-a" - czyli klasa ma wiele części, ale jest czymś więcej niż zbiorem klas. Samolot składa się z części, ale sam w sobie jest czymś więcej - posiada dodatkowe zdolności, takie jak lot. Idąc dalej tym przykładem, poszczególne części mogą występować w różnych specjalistycznych wariantach i wtedy jest to dobry moment na wykorzystanie dziedziczenia.
Dziedziczenie jak i kompozycja to tylko narzędzia, które programiści mają do dyspozycji, więc wybór odpowiedniego narzędzia do konkretnego problemu wymaga doświadczenia. Zatem ćwiczmy i uczmy się na błędach 🙂