Klasy, jakie tworzymy na diagramach klas, łączymy ze sobą za pomocą powiązań. W ten sposób umieszczamy na modelu wiele kluczowych informacji. Zobaczmy, jak tworzyć powiązania poprawnie i elegancko.

W naszym cyklu nie przywiązywaliśmy dotąd zbyt dużej uwagi powiązaniom pomiędzy klasami. Tymczasem to właśnie powiązania (ang. associations) zawierają kluczowe informacje o wzajemnych relacjach pomiędzy obiektami, nadając w tern sposób sens całemu modelowi.

Podstawowe informacje o powiązaniu

Modelując powiązanie między klasami, możemy je opatrzyć wieloma dodatkowymi informacjami. Najczęściej powiązanie opisuje się podając:

  • nazwę powiązania,
  • nazwę ról, jakie pełnią powiązane obiekty,
  • liczebności powiązanych obiektów.

Na rysunku 1. jest pokazane powiązanie zawierające wszystkie wymienione informacje. Powiązanie to nosi nazwę posiada. Obiekt klasy Pojazd uczestniczy w nim w roli własność, zaś obiekt klasy Osoba -- w roli posiadacz. Każdy pojazd może być własnością od jednej do wielu osób, zaś każda osoba może posiadać od zera do wielu samochodów.

 Rysunek 1. Powiązanie między klasami.
Rysunek 1. Powiązanie między klasami.

Żadna z tych informacji nie jest wymagana. Powiązanie bez jakiegokolwiek opisu będzie więc najzupełniej poprawne. W praktyce często pomija się nazwę powiązania oraz nazwy ról, ponieważ zwykle są one oczywiste i wynikają z kontekstu. Jeśli pominiemy liczebności, wówczas przyjmą one domyślne wartości 1. Dobrym zwyczajem jest jednak traktowanie liczebności tak, jakby były wymagane, i podawanie ich nawet wówczas, gdy są równe 1. Sygnalizujemy w ten sposób (sobie i innym użytkownikom diagramu), że przeanalizowaliśmy dane powiązanie i świadomie określiliśmy w nim liczebność jako 1. Brak liczebności oznacza wówczas, że dane powiązanie nie zostało jeszcze przeanalizowane i liczebności obiektów uczestniczących w tym powiązaniu dopiero zostaną określone.

Ciekawe przykłady powiązań

Powiązania możemy modelować pomiędzy dowolnymi klasami. Nic nie stoi więc na przeszkodzie, aby pewna klasa była połączona powiązaniem z samą sobą. Oznacza to, że powiązaniem mogą być połączone dwa (zwykle różne) obiekty tej klasy. W ten sposób najczęściej modeluje się wszelkiego rodzaju hierarchiczne, drzewiaste struktury, dla których nie można z góry określić maksymalnego poziomu zagnieżdżenia, np. strukturę folderów czy hierarchię jednostek organizacyjnych w firmie. Jeden koniec powiązania oznacza wówczas obiekt nadrzędny, zaś drugi - obiekty podrzędne. Przykład takiego powiązania, modelującego drzewo folderów w systemie plików, jest przedstawiony na rysunku 2.

 Rysunek 2. Powiązanie klasy z samą sobą.
Rysunek 2. Powiązanie klasy z samą sobą.

Najzupełniej poprawne jest także łączenie dwóch klas przy pomocy więcej niż jednego powiązania. W przykładzie z rysunku 3. każda rezerwacja hotelowa musi mieć określonego dokładnie jednego klienta głównego oraz dowolną liczbę osób towarzyszących. Z drugiej strony, jeden klient może być dla kilku rezerwacji klientem głównym, a jednocześnie w kontekście innych rezerwacji pełnić rolę osoby towarzyszącej. Jest to zresztą jeden ze sposobów modelowania ról pełnionych przez obiekt (w tym wypadku przez klienta), o których pisaliśmy w artykule "Modelowanie ról na diagramach klas w języku UML".

 Rysunek 3. Klasy połączone dwoma powiązaniami.
Rysunek 3. Klasy połączone dwoma powiązaniami.

Kierunek nawigacji

Powiązaniom na diagramach klas można - oprócz wymienionych na wstępie podstawowych informacji - przypisać także kilka bardziej zaawansowanych, rzadko używanych, ale niekiedy przydatnych własności, precyzujących ich zachowanie. Można na przykład określić kierunki nawigacji po powiązaniu (ang. navigability). Powiązania na rysunkach 1-3 mają nieokreślone kierunki nawigacji, co oznacza, że nie wskazujemy typowego kierunku poruszania się po powiązaniu. Na rysunku 4. jest przedstawiony przykład powiązania, w którym kierunki nawigacji są określone. Strzałka po stronie klasy OddziałNFZ oznacza, że po powiązaniu można nawigować od klasy Pacjent do klasy OddziałNFZ. Innymi słowy, mając obiekt klasy Pacjent, można łatwo określić, w których oddziałach NFZ jest on ubezpieczony. Natomiast krzyżyk po stronie klasy Pacjent wskazuje, że od klasy OddziałNFZ do klasy Pacjent po powiązaniu nie można nawigować. Oznacza to, że odnalezienie pacjentów ubezpieczonych w danym oddziale NFZ jest niemożliwe lub nieefektywne.

Rysunek 4. Powiązanie z określonymi kierunkami nawigacji
Rysunek 4. Powiązanie z określonymi kierunkami nawigacji

UML dopuszcza wszelkie kombinacje tych oznaczeń. Można więc tworzyć powiązania, w których nawigacja jest dopuszczalna w obu kierunkach. Powiązania, w których nawigacja w żadnym kierunku nie jest dopuszczalna, także są formalnie poprawne, choć ich sens praktyczny jest raczej niewielki.

Czy warto stosować oznaczenia kierunku nawigacji na diagramach klas? W większości przypadków są one raczej zbędne i niepotrzebnie zaciemniają diagram. W praktyce warto używać tych oznaczeń tylko dla powiązań jednokierunkowych. Są one wtedy niejako wskazówkami dla programistów, podpowiadającymi, jaki jest typowy kierunek korzystania z powiązania. Wskazówki takie pozwalają programistom zoptymalizować model danych w taki sposób, aby korzystanie z powiązania w określonym kierunku było efektywne.

Zbiory, wielozbiory i porządek

Zgodnie z modelem z rysunku 4., pacjent może być ubezpieczony w wielu oddziałach NFZ. Danego pacjenta z danym oddziałem NFZ może jednak łączyć co najwyżej jeden egzemplarz powiązania. Inaczej mówiąc, dla konkretnego obiektu klasy Pacjent, powiązane z nim obiekty klasy OddziałNFZ  tworzą zbiór (w sensie teoriomnogościowym), a więc nie mogą się powtarzać. Zatem, w danym oddziale NFZ pacjent nie może być ubezpieczony więcej niż raz. Jest to dość naturalne ograniczenie, sprawdzające się w większości powiązań pomiędzy klasami. Jest to opcja domyślna, więc często używamy jej nieświadomi tego, że mamy do dyspozycji także inne możliwości.

Załóżmy, że uznajemy za dopuszczalną sytuację, że pacjent jest ubezpieczony kilka razy w tym samym oddziale NFZ (bo np. opłaca składki z kilku tytułów ubezpieczenia). Aby to uwzględnić na modelu, wystarczy oznaczyć nasze powiązanie po stronie klasy OddziałNFZ etykietą {bag} (zob. rysunek 5). Będzie to oznaczało, że obiekty klasy OddziałNFZ powiązane z pewnym obiektem klasy Pacjent tworzą wielozbiór (ang. bag), a więc mogą się powtarzać. Inaczej mówiąc, pomiędzy pewnym obiektem klasy Pacjent a pewnym obiektem klasy OddziałNFZ może wystąpić kilka egzemplarzy powiązania. Opcja ta jest używana rzadko, choć przyda nam się bardzo w kolejnym odcinku Akademii UML, w którym szczegółowo omówimy klasy asocjacyjne.

Rysunek 5. Powiązanie z wielozbiorem na jednym z końców
Rysunek 5. Powiązanie z wielozbiorem na jednym z końców

Domyślnie obiekty powiązane z danym obiektem nie są uporządkowane (stanowią - jak powiedzieliśmy - zbiór). Załóżmy jednak, że każdy pacjent ubezpieczony w więcej niż jednym oddziale NFZ musi określić preferowaną kolejność korzystania ze swoich ubezpieczeń. Oznacza to, że oddziały NFZ przypisane do pacjenta powinny być uporządkowane. Oznaczamy to napisem {ordered} po stronie klasy OddziałNFZ. Oczywiście tak określona kolejność oddziałów NFZ może być dla każdego pacjenta inna.

Wreszcie jeśli chcemy, aby obiekty powiązane były uporządkowane i jednocześnie mogły się powtarzać, musimy oznaczyć koniec powiązania napisem {sequence}. Jest to połączenie opcji {bag} i {ordered}. W naszym przykładzie użycie napisu {sequence} oznaczałoby, że każdy pacjent może być ubezpieczony kilka razy w jednym oddziale i dodatkowo musi określić kolejność korzystania ze swoich ubezpieczeń.

W następnym odcinku

Za miesiąc przyjrzymy się dokładniej klasom asocjacyjnym. Zobaczymy, kiedy warto je stosować i przeanalizujemy, czym różni się model wykorzystujący klasy asocjacyjne od modelu ze zwykłymi klasami.

Szymon Zioło

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

Dodaj komentarz