poniedziałek, 4 sierpnia 2008

Strategia na wielokrotne dziedziczenie

Wielokrotne dziedziczenie jest be - wszyscy o tym wiemy. Poza oczywistymi problemami logicznymi narusza jedną z głównych zasad OO - utrzymywanie wysokiej kohezji - klasa powinna mieć jak najmniej odpowiedzialności, najlepiej jedną.

Ostatnio zmagałem się z zaprojektowaniem pewnego modelu dostarczonego z fazy analizy. Model analityczny był w sumie dosyć prosty, jednak kobieca intuicja podpowiadała, że będzie sprawiał problemy z rozbudową w przyszłości. Problem domenowy wyglądał tak: mamy zlecenie podania leku; zakładamy, że w przyszłości na pewno dojdą do wykonania inne rzeczy; możemy również zlecać wykonanie czegoś raz lub wielokrotnie (okresowo, wg zadanego wzorca powtarzania).

Model analityczny wyglądał tak: mamy zlecenie, po którym dziedziczy zleceniePodaniaLeku, po którym to z kolei dziedziczą zlecenia jednorazowegoPodaniaLeku jak i wielokrotnegoPodaniaLeku. Czujecie chyba eksplozję kombinatoryczną, która się szykuje w razie pojawienie się nowych rodzajów rzeczy do zlecenia czy nowych polityk powtarzania, lub (o zgrozo) pojawienie się nowych aspektów zlecenia.

Widać tu jak na dłoni wzorzec projektowy Strategii. Postępując zgodnie z tym wzorcem dojdziemy do modelu, gdzie zlecenie składa się ze strategii powtarzania - lub polityki idąc za terminologią Domain Driven. Zlecenie składa się też ze strategii wykonania pewnej czynności. Zlecenie "widzi" jedynie interfejsy strategii, natomiast z jego punktu widzenia implementacje nie są istotne.
Podejście to zapewni nam również zgodność z kolejną zasadą GRASP.

Wzorzec co prawda "widać" z perspektywy projektowej czy implementacyjnej, jednak z perspektywy analitycznej ponoć wygodniej jest pokazać wielodziedziczenie - po typie powtarzania i po typie czynności do wykonania. Jednak ponieważ wielodziedziczenie jest be, więc aby go uniknąć doszło na poziomie analizy do "rzutowania" aspektu powtarzania na aspekt czynności.

Sytuacja z dystansu wygląda śmiesznie: analitycy "szyfrują" rzeczywistość do jednowymiarowej przestrzeni aby uniknąć wielodziedziczenia, a projektanci muszą później dokonać "odszyfrowania" na swoje strategie;)

Dlatego też wspólnie uznaliśmy, że wielodziedziczenie jest git - ale tylko na poziomie analitycznym:P Dzięki niemu projektanci mogą niemal w sposób automatyczny dokonać transformacji modelu analitycznego na implementacyjny model obiektowy oparty na strategiach. Niemal automatyczny ponieważ wszystkie wielowymiarowe aspekty dokładnie "widać" gdy nie są spłaszczone do jednego wymiaru


//===========================
Tak właściwie to nie jestem do końca przekonany czy aby na pewno wielodziedziczenie jest immanentną cechą obiektów ze świata rzeczywistego. Być może jest to jedynie kwestia wyuczonego postrzegania; wynik bezkrytycznego powielania schematów myślowych. Pamiętam, że sam zanim przestawiłem się na agregację/kompozycję, widziałem wszędzie dziedziczenie - wszak kto ma w ręku młotek, ten wszędzie widzi gwoździe.

GRASP jest doskonale opisany w "Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development" autorstwa Craiga Larmana

4 komentarze:

Anonimowy pisze...

Napisałeś coś takiego:
"Tak właściwie to nie jestem do końca przekonany czy aby na pewno wielodziedziczenie jest immanentną cechą obiektów ze świata rzeczywistego."

Rozmyślając nad przykładami wielodziedziczenia ze świata rzeczywistego jakoś nie potrafię znaleźć przykładów właśnie na to wielodziedziczenie. Nie potrafię jeśli chodzi o przedmioty, ale natomiast z ludźmi nie mam takiego problemu:

Każdy z nas dziedziczy coś od swojej matki i ojca od dziadka i babci (albo od sąsiada...). I myślę że to "ludzkie" dziedziczenie wpływa na nasz tok rozumowania właśnie w takim przypadku jak opisywana analiza zlecenia leku.
Człowiek podchodzi do tego jak w życiu: z danego obiektu weźmie sobie (podziedziczy) coś, z drugiego obiektu weźmie (podziedziczy) coś innego i tak powstanie twór mający zestaw pożądanych cech. Ok wszystko ładnie i pięknie, tyle że w "ludzkim" aspekcie dziedziczenia mamy geny progresywne i recesywne, czyli w wyniku mieszkanki genów rodziców jedne cechy są przenoszone dalej na potomka a inne nie.

A jak wiadomo w programowaniu jak dziedziczysz to ciągniesz za sobą bagaż tego co Ci niepotrzebne, nie ma recesywnych elementów które "zanikają" w produkcie końcowym, czyli potomku. Oczywiście można tu na siłę robić jakieś przeciążenia metod "zbędnych" a w nich coś w stylu
throw new UnsupportedException();
ale to się nie trzyma kupy.

Anonimowy pisze...

Ja bym nie traktował tego dziedziczenia tak dosłownie :).
Przypuśćmy że mamy książkę. Książkę można postrzegać jako dokument (liczba stron, format papieru itp), jako utwór (rodzaj treści np. proza, temat itp), jako towar (cena, wymiary, kod produktu), jako materię (masa, skład chemiczny itp). Na razie nie przychodzi mi do głowy więcej.

Apropos kwestii czy rzeczywistym jest wielodziedziczenie czy sztucznym. Zdaje się że naturalne odwzorowanie rzeczywistości nie jest możliwe ze względu na 1. jej złożoność i 2. zależności. Gdybym chciał odwzorować wymienioną książkę w całkowicie naturalny sposób, musiał bym uwzględnuć każdy jej atom, każdy elektron i kwark atomu (a co dalej jest? nawet nie wiemy na pewno) i dalej oddziaływania każdej z tych cząstek z resztą wszechświata a zatem musiał bym stworzyć odwzorowanie całego wszechświata. I tu było by prościej stworzyć kopię wszechświata (1 elektron = 1 elektron zamiast angażować 1bit czy bajt pamięci opisywany wieloma elektronami). Ale tworząc kopię wszechświata w rzeczywistości zwiększył bym dwukrotnie istniejący wszechświat a ich wzajemne oddziaływania sprawiły by że pierwszy nie byłby taki jak przedtem itd. Wynika z tego że nie da się stworzyć żadnego naturalnego modelu istniejących rzeczy a jedynie abstrakcji takich jak np. punkt w przestrzeni (w rzeczywistości nieistniejący). Wszystko inne będzie mniej lub bardziej nieopisane. Im bardziej będzie opisane tym będzie bardziej zawodne (jak kłamca im więcej mówi tym mniej mu wierzą).

Zbigniew Rajewski pisze...

Trafiłem na ten artykuł w roku 2016, czyli 8 lat po jego powstaniu. Nie wiem, czy coś się w Javie do tego czasu zmieniło - zdaje się, że wielodziedziczenie jest nadal na zasadzie składania interfejsów. Dziś w takich językach jak Python dumnie i z ulgą korzystamy z wielodziedziczenia, ale zasady GRASP pozostają nadal aktualne! Po prostu wielodziedziczenie pozwala tworzyć dowolną konfigurację odpowiedzialności obiektu, a w efekcie w zwinny sposób zachowywać maksymalną kohezję na poziomie obiektu jako instancji. Pozostaje jedynie radzenie sobie z kolizjami nazw symboli w dziedziczonych klasach - do ogarnięcia poprzez zachowanie zasad tworzenia symboli w klasach przeznaczonych do wielodziedziczenia.

Sławek Sobótka pisze...

W Javie pojawiły się domyślne implementacje interfejsów. Ale to wciąż nie rozwiąże problemu jaki się wówczas pojawił... chodzi o to, że dany obiekt dostaje "dynamicznie" w trakcie życia pewne umiejętności lub je traci. Bo "składanie" obiektu w trakcie kompilacji to prosty temat;)

Nie wiem jak Pythonie ale Scala czy Ruby ma konstrukcję trait/mixin.

Natomiast w Javie wciąż możemy posłużyć się wzorcem projektowym Role Object Patten, który pozwala na dynamikę i zapewnia elegancką kontrolę typów: http://hillside.net/plop/plop97/Proceedings/riehle.pdf