W poprzednim odcinku Akademii UML przekonaliśmy się, że dziedziczenie zwykle nie sprawdza się podczas modelowania ról i poznaliśmy proste sposoby radzenia sobie z rolami pełnionymi przez obiekty. Zobaczmy, jakie jeszcze techniki można wykorzystać w takich sytuacjach.

OCL (Object Constraint Language) - język pozwalający na zapisywanie ograniczeń dotyczących modelu obiektowego w postaci formalnych wyrażeń o ściśle zdefiniowanej składni i semantyce. Opis języka OCL jest częścią specyfikacji UML.
Nasze rozważania na temat modelowania ról, rozpoczęte w poprzednim odcinku Akademii UML, zakończyliśmy na prostym i uniwersalnym wzorcu, zgodnie z którym role modelujemy jako osobne klasy, które są przypisane relacją kompozycji do głównej klasy, reprezentującej obiekt naszych rozważań (zob. rysunek 1). Wzorzec ten sprawdza się w większości typowych przypadków, jednak dopuszcza istnienie obiektów, które nie mają przypisanej żadnej roli. Jeśli wymagane jest, aby obiekt pełnił zawsze przynajmniej jedną rolę, możemy zapisać to wymaganie w postaci reguły biznesowej (poza diagramem klas, np. w języku OCL) lub wykorzystać technikę przedstawioną na rysunku 2.

 

Rysunek 2. Wykorzystanie klasy abstrakcyjnej reprezentującej rolę
Rysunek 2. Wykorzystanie klasy abstrakcyjnej reprezentującej rolę

Klasa abstrakcyjna

Technika ta różni się od wzorca przedstawionego na rysunku 1. tym, że wprowadza dodatkową abstrakcyjną klasę, reprezentującą dowolną rolę obiektu. Z tej klasy dziedziczą klasy reprezentujące poszczególne role. Zwróćmy uwagę, że liczebność relacji pomiędzy główną klasą Kontrahent a abstrakcyjną klasą RolaKontrahenta jest określona jako 1..*. Oznacza to, że kontrahent musi pełnić co najmniej jedną rolę. Takiego ograniczenia nie można było umieścić na diagramie na rysunku 1, bo wymuszałoby to użycie jednej z ról.

W poprzednim odcinku Akademii UML doszliśmy do wniosku, że dziedziczenie zwykle nie najlepiej nadaje się do modelowania ról. Jak to się stało, że nagle dziedziczenie wróciło do łask? Wówczas próbowaliśmy dziedziczyć poszczególne role bezpośrednio z klasy Kontrahent, co skutkowało ograniczeniami zwykle nieprzystającymi do rzeczywistości. Tym razem dziedziczymy ze specjalnej klasy reprezentującej dowolną rolę kontrahenta, więc jest to jak najbardziej uzasadnione.

Model wykorzystujący klasę abstrakcyjną ma jeszcze jedną zaletę: stosunkowo łatwo można go rozszerzać, dodając - w miarę rozwoju systemu - kolejne role. Dodanie nowej roli sprowadza się bowiem do utworzenia nowej klasy dziedziczącej z klasy abstrakcyjnej. Nie są potrzebne jakiekolwiek inne zmiany w modelu, zaś zmiany w implementacji są minimalne.

Model ten ma jednak także jedną wadę. Możemy wprawdzie elegancko zastrzec, że każdy kontrahent musi pełnić co najmniej jedną rolę, ale nie jesteśmy w stanie zapobiec, aby jeden kontrahent miał przypisanych kilka ról tego samego rodzaju (np. kilka obiektów klasy Klient). Jeśli zależy nam na sformułowaniu takiego ograniczenia, możemy to zrobić jedynie w postaci reguły biznesowej zapisanej poza diagramem klas.

Klasa uniwersalna

Model z rysunku 2. można łatwo rozszerzać, dodając nowe role. Jednak każde takie rozszerzenie wiąże się z przebudową systemu. Tymczasem coraz częściej klienci oczekują, aby tworzone dla nich systemy były konfigurowalne. W naszym przypadku wymaganie to oznacza możliwość dodawania nowych ról z poziomu administracji systemem, bez konieczności jego przebudowy.

Rysunek 3. Wykorzystanie uniwersalnej klasy reprezentującej rolę
Rysunek 3. Wykorzystanie uniwersalnej klasy reprezentującej rolę

Wymaganie to możemy spełnić, wykorzystując model przedstawiony na rysunku 3. Zamiast używać w nim osobnej klasy dla każdej roli, stosujemy jedną, uniwersalną klasę reprezentującą wszystkie role. Nie jest nam więc już potrzebna klasa abstrakcyjna, bo klasę uniwersalną możemy połączyć bezpośrednio z główną klasą Kontrahent. Dodatkowa klasa TypRoli pozwala określić, jakiego typu jest każdy z obiektów klasy RolaKontrahenta. Obiekty klasy TypRoli tworzą słownik dostępnych typów ról. Administrator systemu może zarządzać tym słownikiem, na przykład dodając nowe role w miarę potrzeb.

Rozwiązanie to jest jak widać bardzo elastyczne i konfigurowalne. Ma tylko jedną wadę. Jako że wszystkie role są reprezentowane przez jedną klasę, nie możemy do poszczególnych ról przypisać różnych atrybutów ani połączyć poszczególnych ról relacjami z innymi klasami. W modelu z rysunku 2. mogliśmy połączyć podklasę Klient relacją z klasą Towar, zaś podklasę Dostawca z klasą Surowiec. W modelu z rysunku 3. wszystkie role muszą mieć identyczne atrybuty i relacje i mogą się od siebie różnić jedynie typem roli. Nie możemy zatem pozostać przy dwóch osobnych klasach reprezentujących towary i surowce i musimy je wszystkie ?upchnąć? w jednej klasie Towar. Jest to cena, jaką płacimy za możliwość swobodnej konfiguracji słownika ról bez konieczności każdorazowej przebudowy systemu.

Niezależne klasy

Wszystkie nasze dotychczasowe próby okiełznania ról pełnionych przez obiekty opierały się na milczącym założeniu, że potrzebna nam jest jedna centralna baza kontrahentów, wśród których w jakiś sposób wyróżniamy klientów, dostawców oraz takich kontrahentów, którzy są jednocześnie klientami i dostawcami. Tymczasem nie zawsze jest to sytuacja pożądana. Wyobraźmy sobie dużą firmę produkcyjną, w której kontaktami z klientami zajmuje się Dział Sprzedaży, zaś zamówienia surowców od dostawców organizuje Dział Zaopatrzenia. Te dwa działy są od siebie zupełnie niezależne. Dział Sprzedaży prowadzi swoją własną bazę klientów, zaś Dział Zaopatrzenia - swoją własną bazę dostawców.

Gdybyśmy w takiej firmie wdrożyli system oparty na jednym z dotychczas omówionych modeli, jego użytkownicy doznaliby zapewne wielu rozczarowań i frustracji. Oba działy musiałyby bowiem korzystać ze wspólnej bazy kontrahentów. Pracownicy Działu Sprzedaży nie byliby pewnie zachwyceni, gdyby ktoś z Działu Zaopatrzenia nagle i niepostrzeżenie zmienił dane kontrahenta, który jest jednocześnie klientem i dostawcą, a więc jest obsługiwany przez oba działy.

Rysunek 4. Wykorzystanie niezależnych klas
Rysunek 4. Wykorzystanie niezależnych klas

W takich sytuacjach najlepiej sprawdza się model przedstawiony na rysunku 4. Mamy w nim dwie osobne, zupełnie niezależne klasy Klient i Dostawca. Oznacza to, że w systemie będziemy mieli dwie osobne bazy: bazę klientów, którą będzie zarządzał Dział Sprzedaży, oraz bazę dostawców we władaniu Działu Zaopatrzenia. Każdy dział będzie odpowiedzialny za aktualność wpisów w swojej bazie. Jeśli zdarzy się kontrahent, który jest i klientem, i dostawcą, to jego dane zostaną po prostu wpisane do systemu dwukrotnie: raz do bazy klientów i raz do bazy dostawców. Taki kontrahent będzie więc reprezentowany przez obiekt klasy Klient oraz przez osobny obiekt klasy Dostawca.

Jak więc widzimy, nie zawsze maksymalne ujednolicenie modelu prowadzi do pożądanych rezultatów. Sam fakt, że dwie klasy mają kilka identycznych atrybutów (w naszym przypadku nazwa, adres i NIP), nie jest jeszcze wystarczającym argumentem, aby te klasy w ten czy inny sposób łączyć. Po analizie uwarunkowań organizacyjnych może się bowiem okazać, że stworzenie niezależnych, nie połączonych klas - choć wydaje się nieeleganckie - jest jedynym sensownym biznesowo rozwiązaniem.

W następnym odcinku

Za miesiąc przyjrzymy się dziedziczeniu na diagramach klas. Zobaczymy, kiedy należy, a kiedy nie powinno się go stosować.

Szymon Zioło

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

Dodaj komentarz