Klasy asocjacyjne są interesującym rodzajem powiązań między klasami. Stosujemy je wtedy, gdy potrzebujemy przypisać jakieś atrybuty lub metody do samego powiązania. Zobaczmy, jak ich używać poprawnie i elegancko, oraz kiedy warto je zastępować zwykłymi klasami.

Zwykłe powiązanie pomiędzy klasami nie może mieć atrybutów ani metod. A czasami pewnych atrybutów nie da się przypisać do klas uczestniczących w powiązaniu, więc chcielibyśmy je przypisać do samego powiązania. Wtedy z pomocą przychodzi klasa asocjacyjna (ang. association class), która jest połączeniem powiązania i klasy. Klasa asocjacyjna ? tak jak każda klasa ? może mieć atrybuty, metody i powiązania z innymi klasami, a poza tym zachowuje się tak jak zwykłe powiązanie pomiędzy klasami.

Klient i dostawca

Klas asocjacyjnych użyliśmy już raz w Akademii UML, w artykule "Modelowanie ról na diagramach klas w języku UML". Bazowaliśmy wtedy 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ą.

Klasy asocjacyjne posłużyły nam wówczas do zapisywania dodatkowych informacji o surowcach dostarczanych przez kontrahenta (pełniącego rolę dostawcy) oraz o towarach kupowanych przez tegoż kontrahenta (pełniącego rolę klienta). Sformułowany wtedy model jest pokazany na rysunku 1.

Rysunek 1. Użycie klas asocjacyjnych
Rysunek 1. Użycie klas asocjacyjnych

Zwróćmy uwagę, że atrybutu rabat nie możemy przypisać do klasy Towar, ponieważ każdy z klientów może korzystać z innego rabatu na ten sam towar. Nie możemy go też przypisać do klasy Kontrahent, ponieważ dany kontrahent może mieć różne rabaty na różne towary. Musimy go więc przypisać do powiązania kontrahenta z towarem. Dlatego właśnie zamiast zwykłego powiązania zastosowaliśmy klasę asocjacyjną. Obiekt klasy asocjacyjnej jest tworzony dla każdego wystąpienia (egzemplarza) powiązania pomiędzy pewnym kontrahentem i pewnym towarem. Dzięki temu możemy określić rabat niezależnie dla każdego kontrahenta i kupowanego przez niego towaru.

Podobnie jest z atrybutem czasZapłatyZaDostawę, którego nie można przypisać ani do klasy Kontrahent, ani do klasy Surowiec.

Klasy asocjacyjne a zwykłe klasy

Klasy asocjacyjne nie są wcale tak niezbędne, jak sugerowałby to powyższy przykład. Okazuje się bowiem, że klasę asocjacyjną można łatwo zastąpić zwykłą klasą, która ?przecina? na pół oryginalne powiązanie, tak jak to pokazano na rysunku 2. Ten model jest prawie równoważny modelowi z rysunku 1. Prawie, bo z jednym wyjątkiem. Model z rysunku 2. dopuszcza, aby jeden kontrahent miał kilka obiektów klasy Klient (a więc i kilka rabatów) dla tego samego towaru. Nie jest to raczej sytuacja pożądana. Gdybyśmy chcieli zastrzec, że kontrahent może dla każdego towaru posiadać co najwyżej jeden obiekt klasy Klient, musielibyśmy napisać dodatkową regułę biznesową. Natomiast w modelu z rysunku 1. to ograniczenie jest niejako ?wbudowane?. Dany kontrahent może być połączony z danym towarem co najwyżej jednym egzemplarzem powiązania. Zasada ta dotyczy także klas asocjacyjnych.

Rysunek 2. Zastąpienie klas asocjacyjnych przez zwykłe klasy
Rysunek 2. Zastąpienie klas asocjacyjnych przez zwykłe klasy

To ?wbudowane? ograniczenie wynika z tego, że dla pewnego obiektu klasy Kontrahent, powiązane z nim obiekty klasy Towar tworzą zbiór (w sensie teoriomnogościowym), a więc nie mogą się powtarzać.  W poprzednim odcinku naszego cyklu pisaliśmy jednak, że opcję tą można zmienić, dopuszczając tworzenie kilku egzemplarzy powiązania pomiędzy danym kontrahentem a danym towarem. Oznaczamy to napisem {bag} na końcu powiązania. Wskazuje on, że obiekty powiązane z danym obiektem tworzą wielozbiór (ang. bag), a nie zwykły zbiór.

Jeśli więc szukamy modelu wykorzystującego klasy asocjacyjne, który będzie w pełni (a nie prawie) równoważny modelowi z rysunku 2, czyli takiego, w którym dopuszczalna jest sytuacja, że kontrahent posiada kilka rabatów na ten sam towar oraz (lub) kilka czasów zapłaty za dostawę tego samego surowca, to powinniśmy wykorzystać właśnie wielozbiory. Model taki jest przedstawiony na rysunku 3.

Rysunek 3. Model wykorzystujący klasy asocjacyjne i wielozbiory.
Rysunek 3. Model wykorzystujący klasy asocjacyjne i wielozbiory.

Pamiętajmy przy tym, że nie można z góry orzec, który model jest lepszy ? ten dopuszczający kilka rabatów dla danego kontrahenta na dany towar (rys. 2 i 3), czy ten nie pozwalający na taką sytuację (rys. 1). Każdy z tych modeli jest dobry przy pewnych założeniach. Wszystko zależy od wymagań biznesowych i oczekiwań co do funkcjonalności systemu.

Kiedy klasy asocjacyjne się nie sprawdzają

Załóżmy teraz, że chcielibyśmy ograniczyć nieco funkcjonalność przydzielania rabatów i czasów zapłaty za dostawę. Chcemy mianowicie przydzielać klientowi tylko jeden rabat, stosowany dla wszystkich kupowanych przez niego towarów. Analogicznie, chcemy określać dla dostawcy tylko jeden czas zapłaty za dostawę, obowiązujący dla wszystkich surowców dostarczanych przez tego dostawcę. Model odpowiadający tym założeniom jest przedstawiony na rysunku 4. Tym razem nie można niestety stworzyć równoważnego modelu, wykorzystującego klasy asocjacyjne, ponieważ dla każdego połączenia kontrahenta z towarem bądź surowcem powstaje osobny obiekt klasy asocjacyjnej. My natomiast potrzebujemy jednego takiego obiektu dla kontrahenta, niezależnie od tego, z iloma towarami czy surowcami jest połączony.

Rysunek 4. Przydzielanie rabatów i czasów zapłaty do kontrahentów
Rysunek 4. Przydzielanie rabatów i czasów zapłaty do kontrahentów

Kiedy stosować klasy asocjacyjne?

Z przytoczonych przykładów widać, że klasy asocjacyjne są mechanizmem dość ograniczonym. Korzystając z nich nie można precyzyjnie ustalać liczebności obiektów uczestniczących w powiązaniu. W modelu nie wykorzystującym klas asocjacyjnych liczebnościami obiektów steruje się znacznie łatwiej, dostosowując je do przyjętych założeń. W zasadzie tylko jednego ograniczenia nie można zapisać wprost w modelu bez klas asocjacyjnych: że dany kontrahent może mieć dla danego towaru (surowca) co najwyżej jeden rabat (czas zapłaty za dostawę).

Dlatego warto unikać stosowania klas asocjacyjnych, zastępując je w miarę możliwości zwykłymi klasami i powiązaniami. Zmusi nas to przynajmniej do dokładnego przeanalizowania i poprawnego zapisania liczebności wszystkich klas uczestniczących w powiązaniach.

W następnym odcinku

Za miesiąc zobaczymy, jak duży wpływ na funkcjonalność modelowanego systemu mają liczebności klas biorących udział w powiązaniach. Przekonamy się też, że przechowywanie wielu kopii (redundancja danych) obiektu może niekiedy przynosić lepsze rezultaty, niż współdzielenie jednego globalnego obiektu.

Szymon Zioło

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

Dodaj komentarz