W poprzednim odcinku Akademii UML poznaliśmy sytuacje, w których nie należy stosować dziedziczenia. Zobaczmy, kiedy powinniśmy tworzyć wspólną nadklasę dla kilku klas, a kiedy taka hierarchia klas jest zbędna.

Hierarchia klas

Wyobraźmy sobie, że projektujemy system dla instytucji ubezpieczeniowej, obsługujący ubezpieczenia komunikacyjne pojazdów. Podczas analizy dowiadujemy się, że system ten ma w szczególności działać zgodnie z ustawą ?Prawo o ruchu drogowym?. Tworząc diagram klas dla naszego systemu, musimy w nim oczywiście umieścić klasy reprezentujące pojazdy, które będą podlegały ubezpieczeniom. I tu przychodzi nam z pomocą wspomniana ustawa, w której znajdziemy precyzyjne definicje poszczególnych rodzajów pojazdów, np.:

?Pojazd samochodowy -- pojazd silnikowy, którego konstrukcja umożliwia jazdę z prędkością przekraczającą 25 km/h; określenie to nie obejmuje ciągnika rolniczego?

Na podstawie takich definicji moglibyśmy stworzyć hierarchię klas taką jak na rysunku 1.

Hierarchia klas pojazdów
Rysunek 1. Hierarchia klas pojazdów.

Model ten jest najzupełniej poprawny i nie można mu niczego zarzucić. Jest to jednak znakomity przykład pułapki, w jaką można wpaść modelując rzeczywistość biznesową. Pułapka ta polega na próbie zamodelowania wszystkich możliwych aspektów rzeczywistości, także tych, które są nieistotne z punktu widzenia projektowanego systemu. Tymczasem cała sztuka polega na tym, aby ?oddzielić ziarno od plew?, czyli umieścić w modelu aspekty istotne dla naszego zastosowania, pomijając rzeczy nieistotne.

W tę pułapkę szczególnie łatwo jest wpaść korzystając z dziedziczenia i tworząc hierarchię klas, która wykracza poza zakres projektowanego systemu. Jeśli założymy, że nasza instytucja ubezpieczeniowa zajmuje się przede wszystkim ubezpieczeniami samochodów osobowych, samochodów ciężarowych oraz motocykli, to klasy takie jak PojazdWolnobieżny czy PojazdSzynowy są zbędne, ponieważ wykraczają poza dziedzinę naszego systemu. Niepotrzebne są także klasy PojazdSilnikowy i Pojazd, ponieważ nie mają żadnych atrybutów ani nie są powiązane z innymi klasami, a więc jedynym ich zadaniem jest grupowanie podklas. Umieszczanie w modelu takich klas to zwykłe jego zaśmiecanie.

Nawet jeśli sporadycznie zdarza się naszej instytucji ubezpieczać np. pojazdy szynowe czy specjalne, to zapewne wystarczy wrzucić je do jednego worka, tworząc klasę InnyPojazd i przyjmując, że obiekty tej klasy będą ubezpieczane na zasadach niestandardowych (w przeciwieństwie do np. samochodów osobowych, dla których obowiązuje pewna ustandaryzowana oferta). W ten sposób otrzymamy model przedstawiony na rysunku 2. -- bardziej zwięzły, nie zaśmiecony klasami nie należącymi do dziedziny systemu.

Uproszczona hierarchia klas pojazdów.
Rysunek 2. Uproszczona hierarchia klas pojazdów.

Klasy (pozornie) ze sobą niezwiązane

Nie zawsze jednak unikanie nadklas jest dobrym rozwiązaniem. Czasami warto stworzyć wspólną nadklasę dla klas, które pozornie nie mają ze sobą nic wspólnego.

Wyobraźmy sobie system wspierający naliczanie i wypłaty dopłat dla rolników. Każdy rolnik objęty dopłatami posiada zwierzęta oraz działki rolne (aby być rolnikiem, musi posiadać co najmniej jedno zwierzę lub działkę rolną). Co roku za każde posiadane zwierzę oraz każdą działkę rolną rolnikowi przysługuje dopłata. Załóżmy dla uproszczenia, że kwota dopłaty za zwierzę zależy od jego wagi, zaś kwota dopłaty za działkę -- od jej powierzchni. Aby system mógł naliczać dopłaty, musimy w nim przechowywać dane wszystkich rolników oraz posiadanych przez nich zwierząt i działek. Moglibyśmy to robić zgodnie z modelem przedstawionym na rysunku 3.

Model systemu naliczania dopłat - bez użycia dziedziczenia
Rysunek 3. Model systemu naliczania dopłat -- bez użycia dziedziczenia.

Model ten ma jedną wadę -- dopuszcza istnienie rolników, którzy nie posiadają ani działek, ani zwierząt, co jest niezgodne z podaną definicją rolnika. Tego ograniczenia nie da się niestety zapisać przy pomocy liczebności (1..* zamiast 0..*), bo to oznaczałoby, że każdy rolnik musi posiadać co najmniej jedno zwierzę i co najmniej jedną działkę. W takiej sytuacji zawsze możemy poradzić sobie, zapisując (poza samym diagramem) dodatkową regułę biznesową. Regułę taką zapisujemy najczęściej słownie (?rolnik musi posiadać co najmniej jedno zwierzę lub co najmniej jedną działkę?) lub w postaci wyrażenia (niezmiennika -- ang. invariant) w języku OCL dla klasy Rolnik:

Działka->size + Zwierzę->size >= 1

Zamiast pisać regułę biznesową, warto jednak pomyśleć o modyfikacji samego modelu. Na pierwszy rzut oka wydaje się, że klasy Działka i Zwierzę reprezentują zupełnie różne byty, nie mające ze sobą nic wspólnego. Faktycznie, kawałek ziemi uprawnej rzadko kiedy przypomina zwierzę hodowlane, więc trudno byłoby znaleźć dla nich wspólną nadklasę. Czy rzeczywiście?

Pamiętajmy, że analiza nie polega na jak najwierniejszym odzwierciedleniu każdego detalu rzeczywistości, lecz na zamodelowaniu tych aspektów rzeczywistości, które są istotne dla naszego zastosowania. A w kontekście naszego systemu obsługi dopłat, zwierzę i działka, mimo że tak od siebie różne, mają jedną istotną cechę wspólną: są przedmiotami, za które przysługuje dopłata. I choćby z tego powodu warto stworzyć dla nich wspólną nadklasę, tak jak na rysunku 4.

Model systemu naliczania dopłat - z użyciem dziedziczenia
Rysunek 4. Model systemu naliczania dopłat -- z użyciem dziedziczenia.

Zauważmy, że model ten jest w 100% zgodny z przyjętą definicją rolnika, który musi posiadać przynajmniej jeden przedmiot dopłaty. Ponieważ mamy teraz jedną relację posiadania zamiast dwóch, możemy spokojnie użyć liczebności 1..*. Nie jest nam więc już potrzebna dodatkowa reguła biznesowa.

Model ten ma jeszcze jedną istotną zaletę: można go łatwo rozszerzyć o obsługę kolejnych przedmiotów dopłaty. Jeśli na przykład za jakiś czas zostaną wprowadzone dopłaty za budynki gospodarcze, wtedy wystarczy dodać kolejną podklasę klasy PrzedmiotDopłaty. Podklasa ta będzie posiadała swoją implementację metody obliczDopłatę, która wyliczy wysokość dopłaty np. w oparciu o powierzchnię użytkową budynku. Taki rozszerzony model jest przedstawiony na rysunku 5.

Model systemu naliczania dopłat, rozszerzony o dodatkową podklasę
Rysunek 5. Model systemu naliczania dopłat, rozszerzony o dodatkową podklasę.

Zauważmy, że dodanie do naszego modelu nowej podklasy nie wymaga jakichkolwiek zmian w klasie Rolnik. Obiekt tej klasy, w celu obliczenia przysługującej dopłaty, po prostu sumuje dopłaty wyliczone dla wszystkich swoich obiektów posiadania, niezależnie od tego, czy są to działki, zwierzęta, budynki, czy jeszcze inne, dodane w przyszłości, obiekty.

W ten sposób, tworząc wspólną nadklasę dla dwóch klas pozornie zupełnie ze sobą niezwiązanych, udało nam się stworzyć model elegancki i rozszerzalny.

Reguły Coad?a

Reguły Coad?a

Dziedziczenia należy używać tylko wówczas, gdy spełnione są wszystkie poniższe warunki:

  1. Podklasa oznacza ?bycie rodzajem? nadklasy, a nie ?bycie rolą graną przez? nadklasę.
  2. Obiekt podklasy nigdy nie musi stać się obiektem innej klasy.
  3. Podklasa rozszerza odpowiedzialności nadklasy (a nie zmienia ich ani nie wyłącza).
  4. Podklasa nie rozszerza funkcjonalności klasy pomocniczej.
  5. W modelu dziedziny nadklasa reprezentuje rolę, transakcję lub urządzenie.

(Na podstawie: Bob Tarr, Some Object-Oriented Design Principles)

Zasady dotyczące dziedziczenia, omówione w tym i w poprzednich odcinkach naszego cyklu, powinny pomóc Czytelnikom uniknąć typowych błędów związanych z dziedziczeniem. Stosując dziedziczenie warto też sprawdzić zasadność jego użycia przy pomocy tzw. reguł Coad?a. Jest to pięć podstawowych reguł sformułowanych przez Petera Coad?a, jednego z najbardziej znanych specjalistów zajmujących się UML-em i projektowaniem obiektowym.

Reguła 1. i częściowo 5. dotyczy problemu modelowania ról, przeanalizowanego w artykułach "Modelowanie ról na diagramach klas w języku UML" i "Modelowanie ról na diagramach klas w języku UML - część 2". W pierwszym z nich omówiliśmy także regułę 2. Regułą 3. zajmiemy się w kolejnym odcinku naszego cyklu. Reguła 4. ma zaś charakter techniczny -- dotyczy klas pomocniczych wykorzystywanych w implementacji (np. należących do bibliotek systemowych), więc na szczęście nie ma zastosowania przy tworzeniu modelu dziedziny biznesowej, w którym klasy pomocnicze zwyczajnie nie występują.

W następnym odcinku

Za miesiąc poznamy polimorfizm -- jeden z najważniejszych mechanizmów związanych z dziedziczeniem. Zobaczymy, jakie pułapki związane z polimorfizmem czyhają na nas podczas analizy.

Szymon Zioło

Artykuł został opublikowany w Software Developer's Journal nr 8/2009. 

Dodaj komentarz