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ę...

Wednesday, December 16, 2009

Dziedziczenie zasobnikowe i inne idiotyzmy

Wrzucam przykłady fantazji w dziedziczeniu, które spotkałem (serio!) parę lat temu. Pierwsze można nazwać dziedziczeniem zasobnikowym. Od razu widać co zrobił kreatywny programista. Pisząc fragment systemu raportowego potrzebował przeczytać dane z pliku properties, a że pamiętał coś takiego ze Struts2, to beztrosko sobie odziedziczył.



Drugi kwiatek jest nieco trudniejszy. Dlaczego okrąg dziedziczy z punktu? Otóż dlatego, jak wyjaśnił autor koncepcji, że punkt jest środkiem tego okręgu…

Monday, May 18, 2009

Takie będą systemy jak ich programistów chowanie

Zerknąłem na chwilę na blog Jacka. I przeczytałem następujący akapit.

Jest wiele podobnych [mb: do asm/cglib] tematów, których znajomość pomaga, ale nie będąc warunkiem koniecznym odkłada się je na półkę na bliżej nieokreśloną przyszłość. Możnaby mnożyć takich perełek bez liku, chociażby programowanie w języku funkcyjnym, albo nawet tak podstawowe jak wzorce projektowe


Zawrzałem. Jak, na wszystkie potwory Mordoru, ktoś kto pretenduje do poczytnego bloggera w obszarze Java EE, może pisać podobne rzeczy?!? (Dla sprawiedliwości dodam, że następne niecytowane zdania myśl rozwijają i nieco łagodzą). Po chwili zastanowienia zdałem sobie sprawę, że rzeczywiście nie trzeba znać wzorców, żeby programować, ale również:
  • nie trzeba używać sztućców, żeby się najeść
  • nie trzeba znać języka, żeby się komunikować za granicą
  • nie trzeba myć zębów
  • nie trzeba nosić czapki w zimę, a przy -30 kalesonów
  • nie trzeba czytać książek
  • nie trzeba się rozwijać
  • nie trzeba..., nie trzeba..., nie trzeba...

Żadnej z tych rzeczy nie trzeba. I z pewnością widzimy ludzi, którzy ich nie robią. Od nie robienia tych rzeczy się nie umiera. Przynajmniej nie od razu. Żadna z nich nie jest, wspomnianym przez Jacka, warunkiem koniecznym. Pytanie tylko czy chcemy tak żyć?

[mb: po dyskusji z komentatorami i przemyśleniu sprawy doszedłem do wniosku, że ten akapit to o 1 most za daleko...]Więc jeśli robisz coś, co jak mniemasz kształtuje świadomość programistów i tych, którzy dopiero zaczynają tę przygodę, to kieruj ich w jak najlepszą twoim zdaniem stronę. Nawet jeśli nie dotrą do ideałów, o których piszesz, to:

zostanie ta siła fatalna,
co tobie żywemu na nic, tylko czoło zdobi,
a będzie ich gniotła niewidzialna,
aż zjadaczy chleba w aniołów przerobi


Wpis kończę parafrazą myśli przypisywanej Zamoyskiemu, Modrzewskiemu albo Staszicowi: takie będą systemy, jak ich programistów chowanie!