Thursday, December 17, 2009

Dziedziczenie i kompozycja



Ostatnio wygrzebałem w polskiej blogsferze namiar na Aristotle's Error or Agile Smile. Wynika z niego, że nasza wspaniała obiektowość jest dziełem przypadku (a co nie jest?), a ideologię o modelowaniu świata rzeczywistego dorobili spece od marketingu. Jak było w rzeczywistości nie dociekałem. Skoro już obiektowość, a wraz z nią sztandarowe dziedziczenie.

Dziedziczenie


Na Rysunek 1 jest fragment jakiegoś tam systemu. Pewnie każdy z nas widział podobne rzeczy choćby w Java API, które roi się od podobnych lub o wiele bardziej skomplikowanych konstrukcji.

Choć programiście całkiem nieźle używa się klas zaprojektowanych w ten sposób, to gdy programista chciałby na bazie takich konstrukcji rozwijać swoją aplikację (czytaj: dalej nieograniczenie dziedziczyć), to pojawia się kilka problemów:
  • Aby zrozumieć działanie metody MultiUserRemoteBookStore.findBook() zapoznać się z całą hierarchią dziedziczenia,
  • Faktycznie uruchamiany kod metody tej metody jest rozsiany pomiędzy wiele klas w hierarchii,

  • Wymusza się na nas użycie konstruktorów (parametrowych) z nadklas, których wcale nie mieliśmy zamiaru używać,


  • Dodatkowo nie wiadomo co robią te kostruktory; a nóż przy tworzeniu mojego obiektu coś wybuchnie…


  • Trudno przetestować jednostkowo nową klasę. No, bo jak tu zamokować kod wywoływany w testowanej metodzie poprzez super.findBook()?


Kompozycja

Większość problemów wynikających ze swobodnego dziedziczenia rozwiązuje kompozycja. Zamiast wyprowadzać nową klasę AcmeBookStore z MultiUserBookStore implementujemy główny interfejs BookStoreService a do potrzebnych metod odwołujemy się poprzez delegację. I kawałek kodu:

public class AcmeBookStore implements BookStoreService {

private MultiUserBookStore multiUserBookStore;

public Book findBook( String title ) {
//...
multiUserBookStore.findBook( title );
//...
}

}
(Na marginesie warto zauważyć, że w ten sposób stworzyliśmy implementację Dekoratora.) To rozwiązanie jest zdecydowanie bardziej czytelne i unit-testing-friendly. Kłopot pojawia się gdy nie w architekturze, z którą pracujemy nie istnieje odpowiednik interfejsu BookStoreService. Wtedy już, chcąc nie chcąc, zazwyczaj godzimy się godzimy się na dziedziczenie.

Programowanie poprzez interfejsy

Mając na uwadze w/w mogę zastanawiać się: w jaki sposób mogę projektować architekturę mojego kodu tak, aby nie generował problemów z dziedziczeniem. Pierwszą rzeczą, która przychodzi mi na myśl jest programowanie poprzez interfejsy. Nie oznacza to bynajmniej, że każda nowa klasa UserManager ma swój interfejs UserManagerService, albo (w wersji hardcore) z każdą nową klasą pojawiają się trzy nowe byty w systemie (UserManagerService, UserManagerImpl, AbstractUserManager). Zerknijmy na rysunek: W tej architekturze centralnym punktem systemu (podsystemu, biblioteki) jest interfejs – kontrakt, który mają realizować poszczególne implementacje. Specyfika implementacji np. RemoteBookStore uzyskiwana jest nie poprzez odpowiednie klasy narzędziowe np. RemotingUtilty. Dzięki temu można tworzyć kolejne implementacje specjalizujące się w coraz to nowych rzeczach, bez konieczności dziedziczenia.

Podsumowując

Ujawniły nam się następujące rzeczy:
  • Preferowanie kompozycji ponad dziedziczenie,
  • Programowanie poprzez interfejsy.
    Są to jedne z kluczowych paradygmatów programowania obiektowego. Mamy z nich następujące korzyści:
  • Kod jest czytelny


  • Kod jest otwarty na testowanie.



Ostatecznie pozostaje jedno pytanie: czy dziedziczenie jest złe? Nie, jest bardzo dobre... w pewnych kontekstach… Złe jest jego nadużywanie. Jak więc rozpoznawać, które konteksty są odpowiedni dla dziedziczenia? Pewnie można wykombinować jakieś obiektywne kryteria, ale ja proponuję znać się na intuicję: czy dziedziczenie uprościło czy zagmatwała architekturę? Czy łatwiej testować, czy trudniej? Czy przyjemniej się pisze, czy - przeciwnie - chce Ci się...

5 comments:

  1. Artykuł całkiem ciekawy, faktycznie mało czytałem na temat tego, dlaczego zostało wprowadzone programowanie obiektowe, po prostu przyjmując je takie, jakie jest, dlatego dobrze jest rozszerzyć swoją wiedzę.

    Mama tylko jedno zastrzeżenie - chodzi o użycie słowa "zamokować". Zmień ten czasownik na oryginalną angielską pisownię i nie kalecz naszego ojczystego języka...

    ReplyDelete
  2. Zamokować też mi sie rzuciło w oczy, ale nie sądze że używanie angielskiej wersji było by lepsze. Może lepiej stworzyć jakiś piękny polski wyraz :)

    ReplyDelete
  3. Tommy - kiedy ostatnio coś zbindowałeś, skserowałeś lub skopiowałeś?

    Pamiętaj, że język Twojej matki wyparł język Twojej prababki!

    Gdy się trochę wysilić to można dotrzeć jeszcze głębiej i zapłakać nad pokaleczeniem a następnie wyparciem języka prasłowan czy idąc dalej praludzi;P

    Analiza DNA mitochondrialnego (dziedziczonego niemal niezmiennie przez samice) wskazuje, że WSZYSCY ludzie na ziemi pochodzą od ok 8 Afrykanek. Kto stanie w obronie ich języka - języka praprzodków;P?

    "It's evolution, babe"

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Ja z wykształcenia nie jestem programistą i nie potrafię pisać kodów. Jednak po przeczytaniu artykułu https://www.connecto.pl/system-informatyczny-potrzebny-firmie/ stwierdziłam, że faktycznie w każdej firmie powinny być wdrażane systemy informatyczne.

    ReplyDelete