Kiedy blisko 10 lat temu zacząłem trochę bardziej serio programować obiektowo, byłem ?zauroczony? trzema paradygmatami stojącymi za tym podejściem.

Dla przypomnienia są to (za Wikipedią):

 

  • abstrakcja,
  • hermetyzacja,
  • polimorfizm.

 

Każdy z nich jest ważny i w pewnym sensie definiuje to, jak postrzegamy programowanie obiektowe. Z biegiem lat jednak dostrzegłem wady ostatniego paradygmatu. Dziedziczenie to diabelska pokusa, której trudno się oprzeć. Czasem próbujemy używać go w takich miejscach, w których można by ze spokojem zastosować inne, bardziej eleganckie i satysfakcjonujące rozwiązanie. Do grona alternatyw z pewnością można zaliczyć podejście oparte na  kompozycji, które zyskuje na znaczeniu zwłaszcza wtedy, gdy w projekcie używamy IoC.  

 

Co złego jest w dziedziczeniu?  

 

Jeśli tworzymy rozwiązanie wykraczające poza pojedynczy projekt (tworzymy własne SDK, framework), to istnieje spore prawdopodobieństwo, że dziedziczenie wówczas się nie sprawdzi. W pewnym momencie dojdzie do takiej sytuacji, że potencjalne wdrożenie będzie wymagało zmian w SDK, co może spowodować konflikty w innych korzystających z niego projektach. Równie prawdopodobne jest, że odziedziczymy klasę, w której będziemy modyfikować, lub też nadpisywać, część z wcześniej utworzonych metod. Takie działanie nie jest mile widziane i może w łatwy sposób doprowadzić do złamania niektórych punktów z mnemonika SOLID.   Wykorzystując rozbudowane dziedziczenie, generujemy sporo innych problemów:

 

  • Znacząco utrudniamy tworzenie testów jednostkowych lub całkowicie uniemożliwiamy ich napisanie.
  • Utrudniamy sobie życie przy debugowaniu, w sytuacji gdy metody i właściwości naszego obiektu znajdują się na różnych poziomach dziedziczenia.
  • Łamiemy postanowienia mnemonika SOLID.
  • Tworzymy kod, którego zachowanie jest trudniejsze do przewidzenia.
  • Niwelujemy możliwość wprowadzenia interfejsów, co powoduje, że stajemy się bardziej uzależnieni od konkretnej implementacji.

 

  W czym kompozycja jest lepsza od dziedziczenia?

 

Pewną ciekawą alternatywą jest podejście oparte na kompozycji, które niweluje większość przytoczonych problemów. Stosując ją, możemy świetnie wpasować się w realia SOLID. Tworzymy w tym przypadku klasy, które często implementują konkretne interfejsy. Jeśli we wdrożeniu nie odpowiada nam określona implementacja generatora raportów, to po prostu tworzymy własną, która implementuje metody wcześniej utworzonego interfejsu i jednocześnie wykorzystuje inne, dostępne w naszym projekcie klasy.  

 

W tym przypadku kluczem do sukcesu jest pilnowanie zasady pojedynczej odpowiedzialności poszczególnych klas. Łatwiej jest nam utworzyć pojedynczą nową implementację klasy rozwiązującej konkretny problem i zarejestrować ją w kontenerze IoC, niż głowić się nad tym, co autor klasy bazowej miał na myśli.  

 

Czy powinniśmy zrezygnować z dziedziczenia? Czy jest dla niego jakieś sensowne zastosowanie? 

 

Czy bazując na tym, co napisano powyżej, powinniśmy teraz zabrać swoje wszystkie zabawki z piaskownicy zwanej dziedziczenie i wynieść się do obozu zwolenników kompozycji? Cóż, to zależy od sytuacji. Jest wiele obszarów programowania, w których kompozycja sprawdza się lepiej, ponieważ takie podejście zwiększa dość mocno naszą elastyczność. Z drugiej strony są pewne problemy, w których rozwiązaniu lepiej sprawdzi się stare, poczciwe dziedziczenie. Jednym z nich jest problem drzewa obiektów.  

 

Jeśli spojrzycie na popularne języki programowania wykorzystujące UI, to dostrzeżecie pewną prawidłowość. W większości z nich layout jest tworzony za pomocą drzewa obiektów, co strukturalnie przypomina język XML. Mamy element główny, a później kolejne zagnieżdżenia. Podobnie sytuacja wygląda z perspektywy klas. Mamy klasę główną typu View, Element, UIObject (itd.) oraz klasy potomne z niej dziedziczące.  

 

Łatwo sobie wyobrazić np. taką hierarchię:  

Element => Control => Button => RadioButton

Element => Control => Button => ImageButton itd.  

 

W takich sytuacjach dziedziczenie sprawdza się świetnie, ponieważ kolejne elementy interfejsu zyskują istotne właściwości od swoich rodziców. Ponadto, przeszukując drzewo obiektów, możemy szukać kolejnych obiektów typu „Element”, a później dokonywać odpowiedniego rzutowania, na interesujący nas obiekt docelowy. Czy kompozycja sprawdziłaby się w tym miejscu? Być może, ale mnie bardziej pasuje tu dziedziczenie. W tym przypadku po prostu działa.  

Jerzy Piechowiak

Altcontroldelete.pl

 


Szukasz informacji? Kliknij poniżej: