Menu Zamknij

Jak Dziedziczyć, by nie Zrobić Sobie Krzywdy?

Kompozycja

Dziedziczenie, czy może jednak kompozycja. Wybór nie jest taki łatwy. Dla niektórych programistów jest to wręcz odwieczny dylemat. Często zastanawiają się, które z tych rozwiązań będzie lepsze. Każde z nich ma swoje plusy i minusy. No i rzeczywiście, sprawa nie jest aż tak prosta, jakby się to niektórym mogło wydawać.

  • Każde dziedziczenie można zastąpić kompozycją!

Wystarczy połączyć to z prostym delegowaniem metod. Konstruując kod w taki sposób, można osiągnąć całkiem zadowalający efekt.

Nasuwa się więc bardzo niewygodne pytanie –

  • Po co w ogóle dziedziczyć, skoro i tak można zastąpić to kompozycją?

W tym wpisie postaram się poruszyć tą nurtującą kwestie. Rzucić na nią trochę światła i podzielić się moimi przemyśleniami na ten temat. Przeczytajcie, a może chociaż trochę rozjaśni to również waszą perspektywę.

Czy Dziedziczenie jest W ogóle Potrzebne do Szczęścia?

W poniższym fragmencie kodu dokładnie widać, że można poradzić sobie bez niego.

class BaseClass {
   void doSomething(){
      //do something
   }
};

class AdvencedClass{

   private final BaseClass base;

   AdvencedClass(final BaseClass base){
      this.base = base;
   }

   void doSomething(){
      base.doSomething();
   }

   void doSomethingElse(){
       //do something else
   }
}
  • Czym więc jest to dziedziczenie?

Dziedziczenie to przejmowanie przez klasę pochodną wszystkich niestatycznych pól i metod klasy bazowej.

  • Jakie z tego płyną dla nas korzyści?

Dziedziczenie wprowadza do programowania całkiem ciekawą abstrakcję. Czasami wręcz pomaga ona odnaleźć się w naszym kodzie. Dzięki dziedziczeniu możemy łatwo stwierdzić, że coś jest pewną odmianą czegoś, co już dobrze znamy. Natomiast kompozycja, reprezentuje zupełnie inną relację. W jej przypadku mówimy raczej o prostym zawieraniu się elementów.

Czy możemy sobie pozwolić na odrzucenie tej abstrakcji pojęć? – Rozróżnienia pomiędzy bycia odmianą pewnej rzeczy a prostym zawieraniem się. Wydaje mi się, że nie. Jeżeli taka abstrakcja istnieje w ludzkich umysłach, to będzie ona pomocna również w programowaniu.

Właściwie, to czym są języki programowania, jak nie pewnym sposobem na przekazanie swoich intencji do maszyny. Nie powinniśmy sami ograniczać sobie tych możliwości, które daje nam dziedziczenie. Czytelność kodu mogłaby na tym znacząco stracić, a możliwości jego konstrukcji stałyby się dość ubogie.

  • Dziedziczenie jest łatwe!

Dodatkowo śmiem stwierdzić, że dziedziczenie jest łatwiejsze w implementacji niż kompozycja. Wystarczy dodać słówko kluczowe extends i wywołać konstruktor klasy bazowej.

  • Nie wymaga wstrzykiwania komponowanego obiektu.
  • Nie ma ryzyka związanego z tym, że ten obiekt będzie nullem.
  • Nie wymaga delegowania metod.

Jak widać, dziedziczenie ma jednak jakieś tam zalety. Nie stosowanie go w pewnych przypadkach, byłoby po prostu celowym utrudnianiem sobie życia. To prawda, że dziedziczenie szykuje na nas pewne pułapki. Warto się jednak z nim trochę lepiej zaprzyjaźnić, bo ma też sporo zalet.

To Samo, ale Tylko trochę Inne

Dziedziczenie pozwala budować na pewnym fundamencie. Ta podstawa to dobry materiał wyjściowy, do zrobienia czegoś więcej, czegoś bardziej zaawansowanego. Klasa pochodna powinna być raczej wersją rozszerzoną klasy bazowej. To trochę tak jak z pakietami Basic, Standard i Premium w sprzedaży. Zazwyczaj każdy kolejny będzie miał to samo co poprzedni plus jakieś tam dodatki. Podobnie sprawa ma się z dziedziczeniem.

Rozszerzanie klasy
Rozszerzanie klasy powinno być jak rozszerzania oferty – Widać, że każda następna oferta ma wszystkie elementy z poprzedniej.

Dziedziczenie przede wszystkim właśnie po to jest, by zrobić tę samą rzecz, ale trochę lepszą, sprytniejszą, bardziej zaawansowaną, bogatszą, która nadal jest tym samym, ale jednak trochę innym.

  • Buduj na tym, co już znasz i działa!

Możemy wtedy zagwarantować, że to, co zostało wypracowane do tej pory, będzie nadal działać i będziemy mogli z tego korzystać, mimo że w danej chwili wprowadzane są nowe ulepszenia. Takie podejście mocno wpisuje się w retorykę OCP.

Dziedzicznie daje jeszcze jedną ciekawą możliwość. Pozwala na to, żeby obiekty miały swoje drugie oblicze, które objawi się tylko tam, gdzie tego chcemy.

Na granicy Światów

Dziedziczenie
Dwa światy, dwie klasy, jeden obiekt.

W każdej aplikacji możemy wyłonić jakieś konteksty, jak i również pewne poziomy abstrakcji. Pewne odmienne światy z trochę różnym postrzeganiem rzeczywistości. W naszych systemach dane podróżują, przez te różne konteksty, które potrafią patrzeć na nie w tak odmienny sposób. Często wymagania co do zestawu tych danych, jak i sposobu ich przetwarzania są bardzo zróżnicowane.

Czasami chcemy, by nasz obiekt „przemycił” trochę danych technicznych niezwiązanych z biznesem lub posiadał pewne dodatkowe zachowania wymuszone aspektami czysto technicznymi. Implementowanie tych mechanizmów lub umieszczanie ich w klasach biznesowych byłoby jawnym łamaniem SRP. Jednak przeniesienie tych aspektów do klasy bazowej, może być już jak najbardziej OK.

// package business
class BusinessClass extends TechnicalClass {
   //business data & logic
};
class BusinessService {
   //fields & constructors
   void doBusinessWork(BusinessClass businessObject){
      //business logic
      technicalComponent.doTechnicalWork(businessObject);
  }
};

// package technical
public class TechnicalClass {
   void executeLogic(){
      //execute technical logic
   }
};
public class TechnicalComponent{
   public void doTechnicalWork(TechnicalClass technicalObject){
      technicalObject.executeLogic();
   }
};

Najprostszym przykładem jest klasa Object. Niejawne dziedziczenie po tej klasie pozwala na to, że każdy obiekt będzie miał cechy, które są wykorzystywane przez podstawowe mechanizmy Javy.

To właśnie takie rozwiązania pomagają w rozdzieleniu odpowiedzialności. Mimo że w aplikacji dany byt związany jest z dwoma kontekstami, to ich definicje są odseparowane. Pamiętajmy, że to podział kodu na klasy, a nie na obiekty poprawia jego czytelność.

Uważam, że to naprawdę dobry sposób na czytelne przeplatanie biznesu z aspektami bardziej technicznymi.

Gdzie jest Przewaga Kompozycji?

To prawda. Dziedziczenie ma swoje wady. Jest trochę toporne i może powodować wiele problemów, które nie występują w przypadku kompozycji.

  • Większa elastyczność!

Kompozycja jest rozwiązaniem dużo bardziej elastycznym. Można w każdej chwili zdefiniować komponowany obiekt w czasie działania aplikacji. Możemy nawet podmienić ten obiektu. Możemy również „w locie” dobrać odpowiednią implementację. Możemy także pozwolić sobie na nieposiadanie komponowanego obiektu. Jak widać, możemy dużo, czasami aż za dużo.

Gdy złożoność problemu rośnie, kompozycja często okazuje się dużo lepszą opcją. W wielu sprawach daje nam wolną rękę do sprytnego obejścia utrudnień, które wtedy napotykamy.

  • Lepsza herymetyzacja!

Kompozycja pozwala również na większą hermetyzację. Komponowane obiekty zazwyczaj będą prywatne, a ich metody publiczne nie muszą przecież być delegowane na zewnątrz. W przypadku dziedziczenia wszystkie metody dostępne publicznie w klasie bazowej będą również tak samo dostępne w klasie pochodnej, nawet jeżeli tego bardzo nie chcemy.

Dodatkowo zaznaczę, że kompozycja nie narzuca na nas kontraktu, więc nie musimy się tu tak martwić o przestrzeganie LSP. Fałszywa realizacja i łamanie zasady podstawienia Liskov to w przydatku dziedziczenia częste zjawisko.

Nie daj Się zwieść Fałszywemu Dziedziczeniu!

Jak już pisałem wyżej, dziedziczenie daje możliwość budowania na fundamentach. Jednak gdy te fundamenty nie są odpowiednie, to całość mocno się chwieje i w każdej chwili może runąć.

Dziedziczenie tylko ze względu na samo podobieństwo pól i metod nie ma sensu. Rzeczy mogą być podobne do siebie, ale nie być tym samym. Używanie dziedziczenia w tym przypadku może być bardzo zgubne. Przesłanką do dziedziczenia powinno być takie stwierdzenie.

Tak to jest tym samym, ale tylko trochę innym!

Przykłady:

  • Tak! Ta szafka jest meblem, ale takim co wisi na ścianie.
  • Tak! Ten pies jest zwierzęciem, ale takim co merda ogonem.
  • Tak! Ten samochód jest pojazdem, ale takim co porusza się drogą.
  • Tak! Ta migracja jest wątkiem, ale takim, który migruje dane.
  • Tak! Ten użytkownik jest encją, ale taką, która przechowuje dane użytkownika.

Bardzo ważną, kwestią jest zachowanie kontraktu. Zastanów się, czy możesz podmienić obiekt klasy potomnej za obiekt klasy bazowej i system będzie nadal poprawnie funkcjonował. Samochód musi mieć wszystkie cechy i zachowania pojazdu. Inaczej nie będzie pojazdem. To proste!

Fałszywe dziedziczenie
Oczywiście, że Kaczor Donald nie jest kaczką rozumianą jako zwierzę. Oto przykład fałszywego dziedziczenia.

Rzeczy mogą być podobne, ale jednak być zupełnie czymś innym. Ich podobieństwo może być bardzo złudne. Kod tych rzeczy może być bardzo podobny, ale ich kontekst, może być tak odmienny, że ujednolicanie go, może szybko przysporzyć tylko więcej problemów niż pożytku. Nie powinieneś bać się w takim przypadku duplikować trochę kodu. Taka duplikacja często okaże się pozorna i nie trwała.

Fałszywe dziedziczenie
Klasa, która dziedziczy ze złych powodów, zawsze prosi się o śmierć.

Jeżeli doprowadzisz to fałszywego dziedziczenia to powstanie klasa, w której będą ścierać się zupełnie dwa bardzo odmienne konteksty. Będzie ciągła próba łatania tej dziurawej konstrukcji. Może to prowadzić do późniejszych zmian w klasie bazowej, które mogą generować jeszcze większe problemy.

Częstym objawem fałszywego dziedziczenia jest niewykorzystywanie odziedziczonych metod lub zabijanie ich kołkiem o nazwie `NotImplementedException`. W niektórych skrajnych przypadkach nadpisanie metod ich pustą wersją lub zwracaniem null’a.

Poznaj dziedziczenie w Praktyce!

Jesteś gotów, na bliższe spotkanie z dziedziczeniem? Mam dla ciebie pewne wyzwanie. Postaraj się napisać prostą grę 2D, w której będzie sporo różnego rodzaju obiektów na mapie. Gracz, wrogowie, kamienie, drzewa, bagna, przeszkody, pułapki, apteczki, pancerz, amunicja, broń, pieniądze, pociski lub inne wystrzeliwane obiekty.

Sam w ten sposób nauczyłem się dziedziczenia i poznałem jego jasne i ciemne strony. Według mnie to naprawdę świetny sposób na naukę, przy okazji pozwala na trochę fanu. No i wiadomo, nic tak nie uczy, jak praktyka.

Aqua Wars
Aqua Wars – Moja gra z ‎2013 roku ;D

To tylko Dwa różne Narzędzia

Dziedziczenie i kompozycja to po prostu dwa różne narzędzia. Sam musisz zdecydować, które z nich lepiej nadaje się do twojego problemu. Niestety w programowaniu, jak i w życiu, nie zawsze mamy na wszystko sprawdzone recepty. Mam jednak nadzieje, że po przeczytaniu tego wpisu, te decyzje okażą się pewniejsze i bardziej trafione.

Zachęcam wszystkich do dzielenia się swoimi przemyśleniami na temat dziedziczenia i kompozycji w komentarzach. Jeżeli podejmiecie wyzwanie, to możecie podrzucić jakiś namiar na swoją grę.

Please follow and like us:
Skuteczna refaktoryzacja w 10 krokach!

Odbierz Darmowy Poradnik o Refaktoryzacji!

Poznaj kilka prostych technik i wprowadź nową jakość w swoim projekcie.

Dzięki za dołączenie do mojej listy.

Coś poszło nie tak :( Spróbuj jeszcze raz.