Pracownik firmy jest sprzedawcą lub inżynierem. Siedziba firmy jest jej centralą lub oddziałem. Kontrahent jest klientem lub dostawcą. To tylko kilka przykładów często spotykanej sytuacji, gdy dany obiekt pełni wobec innych obiektów pewne role. Zobaczmy, w jaki sposób można modelować role na diagramach klas.

Modelowanie ról wydaje się być zagadnieniem prostym i intuicyjnym. A jednak powoduje wśród analityków i projektantów sporo nieporozumień, których efektem są diagramy klas niepoprawnie odzwierciedlające rzeczywistość biznesową. Modelując role rzadko bowiem odpowiadamy sobie na dwa zasadnicze pytania:

  • czy obiekt może pełnić tylko jedną rolę w danym czasie, czy może ich pełnić kilka jednocześnie?
  • czy obiekt może zmienić pełnioną rolę, czy przez całe swoje życie występuje w jednej roli?

W zależności od odpowiedzi na te pytania, powinniśmy wybrać odpowiedni sposób modelowania ról. A mamy do wyboru kilka technik. Omówimy je na przykładzie firmy produkcyjnej, która prowadzi bazę swoich kontrahentów. Kontrahent może kupować towary od naszej firmy -- jest wtedy nazywany klientem. Może także dostarczać naszej firmie surowce -- jest wtedy dostawcą.

Role jako podklasy

Role często intuicyjnie modelujemy jako podklasy, wychodząc z założenia, że klient i dostawca są szczególnymi przypadkami kontrahenta. Tworzymy wówczas model taki jak na rysunku 1. Model ten ma dwa bardzo istotne ograniczenia, wynikające z samej istoty obiektowości:

  • każdy obiekt może być obiektem tylko jednej klasy,
  • jeśli utworzymy obiekt danej klasy, to nie może on w trakcie swego życia zmienić tożsamości i zawsze już będzie obiektem tej klasy.

Modelowanie ról przy pomocy podklas

Rysunek 1. Modelowanie ról przy pomocy podklas.

W naszym przykładzie oznacza to, że nasi dostawcy nie mogą być obsługiwani jako klienci, zaś klienci nie mogą być jednocześnie dostawcami. Nie można bowiem utworzyć obiektu, który jest jednocześnie obiektem klasy Klient i Dostawca. Co więcej, nawet jeśli zaprzestaniemy współpracy z pewnym dostawcą, to do momentu usunięcia go z bazy kontrahentów nie możemy obsługiwać go jako klienta. Jest bowiem reprezentowany przez obiekt klasy Dostawca i nie może się przeistoczyć w obiekt klasy Klient.

W literaturze spotyka się próby obejścia drugiego ograniczenia, opierające się na spostrzeżeniu, że przecież obiekt klasy Dostawca można usunąć, tworząc w jego miejsce obiekt klasy Klient. Jednak obchodzenie ograniczenia w ten sposób jest rozwiązaniem bardzo nieeleganckim i zaciemniającym model systemu. Wymaga tworzenia nowego obiektu, kopiowania atrybutów i usuwania starego obiektu. Jeśli chcemy, aby obiekt mógł zmieniać swoją rolę w trakcie swego życia, wystarczy zastosować jeden z modeli omówionych w dalszej części artykułu.

Nie znaczy to bynajmniej, że wykorzystanie podklas do modelowania ról jest zawsze błędem. Wszystko zależy od tego, jak odpowiemy na pytanie, czy obiekt może mieć w swoim życiu wiele ról, czy tylko jedną. Jeśli świadomie przyjmiemy wspomniane dwa ograniczenia, to model wykorzystujący podklasy znakomicie spełni swoje zadanie.

W naszym przykładzie sensowne wydaje się jednak założenie, że dostawca może być jednocześnie klientem (na przykład dostawca wycieraczek dla firmy produkującej samochody może jednocześnie kupować samochody od tej firmy). Dlatego model wykorzystujący podklasy w naszym przykładzie nie jest poprawny.

Role jako związki

Najprostszym sposobem obejścia wspomnianych ograniczeń związanych z dziedziczeniem jest zamodelowanie ról po prostu jako relacji pomiędzy klasami. Nazwy ról możemy zapisać na końcach relacji, przy klasie, która pełni daną rolę. Takie rozwiązanie jest przedstawione na rysunku 2.

Modelowanie ról jako relacji 
Rysunek 2. Modelowanie ról jako relacji.

Zgodnie z tym modelem jeden kontrahent może być jednocześnie klientem i dostawcą -- wystarczy, że powiążemy go relacjami zarówno z towarami, które kupuje, jak i z surowcami, które dostarcza.

Trudno sobie wyobrazić bardziej eleganckie rozwiązanie. Jest proste -- nie wymaga użycia dodatkowych klas -- a jak wiadomo prostota jest bardzo pożądaną cechą modeli informatycznych. Ma tylko jedną wadę: nie pozwala przypisać atrybutów do poszczególnych ról. Role są bowiem relacjami, a te nie mogą mieć atrybutów.

Z tym ograniczeniem można jednak sobie łatwo poradzić. Wystarczy ?przeciąć? relacje na pół, wstawiając dodatkowe klasy reprezentujące role. W klasach tych umieszczamy atrybuty, które chcielibyśmy przypisać do ról. Ponieważ obiekty tych klas są ściśle zależne od obiektu głównego (w naszym przykładzie kontrahenta), między klasą główną a klasami reprezentującymi role możemy zastosować kompozycję. Takie rozwiązanie jest przedstawione na rysunku 3.

Kontrahenta, który jest dostawcą, poznamy po tym, że z obiektem reprezentującym tego kontrahenta jest związany obiekt klasy Dostawca. Nic nie stoi oczywiście na przeszkodzie, aby ten sam kontrahent był jednocześnie klientem i miał stowarzyszony obiekt klasy Klient.

Bardzo podobne rozwiązanie można uzyskać stosując klasy asocjacyjne (ang. association classes). Klasa asocjacyjna jest związana z relacją między dwoma innymi klasami. Dla każdego egzemplarza tej relacji jest tworzony osobny obiekt klasy asocjacyjnej. Jest to więc technika pozwalająca pośrednio przypisać atrybuty do relacji (poprzez ich umieszczenie w klasie asocjacyjnej).

Rozwiązanie wykorzystujące klasy asocjacyjne jest przedstawione na rysunku 4. Różni się ono od rozwiązania z rysunku 3. -- poza zastosowaną notacją -- tylko jednym drobnym szczegółem. Wg rysunku 3., kontrahent dostarczający wiele różnych surowców ma przypisany tylko jeden obiekt klasy Dostawca, wspólny dla wszystkich dostarczanych przez niego surowców. Czas zapłaty za dostawę jest więc określony tylko raz dla każdego dostawcy. Natomiast zgodnie z rysunkiem 4., osobny obiekt klasy asocjacyjnej Dostawca jest tworzony dla każdego surowca dostarczanego przez dostawcę. Zatem czas zapłaty za dostawę trzeba określić osobno dla każdego surowca dostarczanego przez danego dostawcę. Analogiczna różnica pomiędzy tymi dwoma modelami dotyczy klasy Klient i jej atrybutu rabat.

W następnym odcinku

Rozwiązania przedstawione na rysunkach 3. i 4. pozwalają na utworzenie kontrahenta, który nie ma przypisanej żadnej roli. W kolejnym artykule zobaczymy, jak można sobie radzić, gdy obiekt musi mieć przynajmniej jedną rolę. Poznamy także kolejne, niekiedy bardzo zaskakujące techniki modelowania ról.

Szymon Zioło

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

Komentarze  

# Proszę o wyjaśnienie co do zastosowanej relacjiDamian 2012-03-15 14:38
Czy na rys.3 właściwe jest użycie kompozycji? Czy nie oznacza to, że Ten sam kontrahent po tym, jak przestanie być klientem automatycznie przestaje być i dostawcą? Czy kompozycja przewiduje w ogóle,a by posiadany obiekt (kontrahent) był tworzony poza klasą (klient lub dostawca) i udostępniany do modyfikacji innym?
Odpowiedz
# Re: Proszę o wyjaśnienie co do zastosowanej relacjiSzymon Ziolo 2012-03-16 11:00
W kompozycji przedstawionej na rys. 3 Kontrahent jest całością (kompozytem), a Klient i Dostawca - częściami podrzędnymi (składowymi). Jaką logikę oferuje kompozycja? Zajrzyjmy do źródła: "Composite aggregation is a strong form of aggregation that requires a part instance be included in at most one composite at a time. If a composite is deleted, all of its parts are normally deleted with it." (OMG UML Superstructure, version 2.2, str. 41) To są dwie cechy kompozycji: część może być częścią co najwyżej jednej całości, zaś usuwając całość, usuwamy też części (usuwanie kaskadowe). Tylko tyle. Nic więcej. Nie ma żadnych innych ograniczeń. Części składowe kompozycji są zwykłymi klasami i nie ma powodu aby np. nie uczestniczyły w związkach z innymi klasami (tu: Surowiec i Towar). Nie ma powodu, aby obiekty klas Surowiec i Towar nie mogły modyfikować powiązanych z nimi obiektów klas Klient i Dostawca, o ile oczywiście klasy Klient i Dostawca mają odpowiednie metody pozwalające na takie modyfikacje. Zasada usuwania kaskadowego w kompozycji działa tylko w jedną stronę: jeśli usuniemy kompozyt (tu: kontrahenta), to usuną się też jego role (klient i dostawca). W drugą stronę to nie działa - jeśli usuniemy obiekt klasy Klient, to zawierający go (w sensie kompozycji) obiekt klasy Kontrahent nadal istnieje. Obie kompozycje są niezależne od siebie, a więc usunięcie powiązanego z danym kontrahentem obiektu klasy Klient nie powoduje usunięcia obiektu klasy Dostawca powiązanego z tym samym kontrahentem.
Odpowiedz

Dodaj komentarz