pprogramowanie;

// blog o programowaniu i branży IT

rss

Diagramy klas UML

30 września 2022, kategoria: Język UML
diagram-klas-uml

Umiejętność czytania oraz tworzenia diagramów klas UML jest podstawą w przypadku zawodu programisty. Z takimi diagramami będziesz spotykał się w przeciągu całej swojej kariery. Diagramy klas UML są zawsze obecne we wszelkiego rodzaju dokumentacjach.

Diagramy klas UML

Język UML jest językiem służącym do graficznego modelowania aplikacji oraz systemów. Najczęściej wykorzystywany jest do modelowania diagramów klas.

Diagram klas UML jest statycznym diagramem, przedstawiającym strukturę aplikacji bądź systemu w paradygmacie programowania obiektowego.

Diagramy klas UML mogą pokazywać strukturę całego systemu bądź tylko jego części (i najczęściej tak jest). Definiują jakie metody i pola powinna zawierać dana klasa. Pokazują one także część ich implementacji. Oczywiście, diagram przedstawia tylko typy obiektów bez ich instancji - za to odpowiedzialny jest diagram obiektów UML.

Należy pamiętać, że informacje zawarte w diagramie klas UML są poglądowe. Mają ułatwić implementację klas oraz ukazać zasadę funkcjonowania systemu.

Na diagramie UML nie da się zawrzeć wszystkich informacji o danej klasie. Gdyby tak było, wtedy diagramy klas byłyby graficzną reprezentacją kodu - a tak nie jest. Jeżeli w diagramie klas nie ma jakiegoś składnika, to nie można zakładać, że takowy nie istnieje w modelowanej aplikacji.

Jeżeli w diagramie klas UML brakuje jakiegoś składnika, nie oznacza to, że taki nie istnieje w systemie. Poprawne jest także całkowite ukrycie składników klasy, pozostawiając tylko jej nazwę, ale tylko w wypadku gdy nie jest to klasa silnie decydująca o modelu systemu.

Reprezentacja klas w UML

W języku UML istnieją pewne standardy, mówiące w jaki sposób przedstawiać różne elementy. Każdy obiekt reprezentowany jest przez jeden prostokąt, w którym zawarte są jego składniki.

W prostokącie może znajdować się nazwa elementu, nazwa elementu wraz z polami bądź wszystkie składniki, czyli: nazwa, pola i metody. Dodatkowo do składników elementu można dołączać informacje o typie, typie zwracanym lub argumentach. Dobrym zwyczajem jest także określanie poziom dostępu do składników klasy.

Składniki klasy mogą posiadać różne modyfikatory dostępu:

Przykładowa diagram klasy na dwa sposoby:

klasy-uml

Reprezentacja klas abstrakcyjnych i interfejsów w UML

Jeżeli chodzi o klasy abstrakcyjne sytuacja jest prosta: przedstawia się je tak jak zwykłe klasy ale ich nazwa oraz składniki pisane są kursywą. To jedyna różnica w przedstawianiu klas abstrakcyjnych za pomocą UML.

Interfejsy przedstawia się w UML tak jak klasy, jednak ich nazwa musi zostać poprzedzona słowem kluczowym <<interface>>. Słowo to jest standardem języka UML.

Zwyczajem, który osobiście stosuję, jest rozpoczynanie nazw interfejsów od dużej litery i. Nie wynika to absolutnie ze standardu UMLa, jednak pozwala łatwiej analizować diagram. Jest to zasada znana programistom C#, gdzie nazwy interfejsów poprzedza się właśnie literą i, jednak nieobowiązująca w innych językach.

Implementację interfejsu do danej klasy oznacza się pustym białym grotem strzałki, który znajduje się na końcu przerywanej linii. Oto przykład:

interfejsy-uml

Klasa implementująca interfejs musi implementować jego metody. W przypadku klasy abstrakcyjnej, klasa która dziedziczy, musi implementować metody abstrakcyjne.

Związki pomiędzy klasami w UML

Diagramy klas UML byłyby bezużyteczne, gdyby nie można było określać powiązań jakie występują między klasami. Poniżej opiszę wszystkie rodzaje związków między klasami w UML oraz dołączę do nich przykłady. Krótka infografika podsumowująca:

diagram-klas-uml

W relacjach pomiędzy klasami mogą występować cechy krotności:

Krotności umieszczamy po obu stronach zależności. Jeżeli krotność nie jest podana należy przyjąć, że ma wartość 1.

Zależność

Zależność (ang. dependency) jest najsłabszą relacją jaka może występować pomiędzy dwoma klasami. Zależność oznacza, że jedna klasa chwilowo wykorzystuje drugą. Zmiana w jednej z klas może spowodować konieczność zmian w drugiej klasie.

zaleznosc-uml

Powyższą zależność należy czytać w sposób: klasa portfel jest zależna od klasy Pieniadze.

Przykładem na zobrazowanie zależności między klasami jest przekazywanie argumentów do funkcji:

public class Portfel
{
    public void Dodaj(Pieniadze pieniadze) { }
}

Kolejnym przykładem jest wartość zwracana funkcji:

public class Portfel
{
    public Pieniadze Wyjmij(int kwota) { }
}

System powinien mieć jak najmniej zależności, jednak w miarę rozwoju projektu nie da się ich uniknąć. Aby pozbyć się zależności można jako parametr funkcji przekazać kilka typów prostych, zamiast jednej klasy. Spowoduje to zmniejszenie ilości zależności, jednak rodzi inne problemy (tzw. nadużywanie typów prostych (ang. primitive obsession)).

Asocjacja

Asocjacja (ang. association) jest silniejszym rodzajem relacji niż zależność. Asocjacja oznacza, że jedna klasa wykorzystuje drugą, jednak siła relacji jest niedookreślona.

Asocjacje mogą być jednokierunkowe, dwukierunkowe oraz nieokreślone (bez grotów na końcu linii). W diagramach klas UML asocjacji używa się bardzo często. Wynika to z faktu jej wysokiego poziomu abstrakcyjności.

Istnieje pewna dowolność w czytaniu asocjacji. Aby pomóc w ich odczytywaniu często są one dodatkowo opisane. Dowolność w czytaniu asocjacji jest spowodowana tym, że nie wiemy, jak mocno zależne są od siebie obiekty.

asocjacja-uml

Aby sprecyzować siłę asocjacji należy użyć jej szczególnych podrodzajów czyli agregacji częściowej lub całkowitej.

Agregacja częściowa

Agregacja częściowa (ang. aggregation) jest szczególnym rodzajem asocjacji. Jest to związek dwóch klas w formie relacji całość-część. Ważnym jest fakt, że usunięcie klasy całość nie wpływa na istnienie klasy części, tak więc klasy podlegające agregacji częściowej mają niezależny cykl życia (mogą istnieć niezależnie od siebie).

W Agregacji częściowej element częściowy należy do elementu głównego, jednak nie jest od niego zależny. Usunięcie elementu głównego nie wpływa na usunięcie elementu częściowego. Element częściowy może także należeć do wielu elementów głównych. Sprawa wydaje się skomplikowana, ale rozważmy przykład:

agregacja-czesciowa

Ten prosty diagram agregacji częściowej należy czytać w następujący sposób: klasa katalog ma klasę dokument.

Oto kod obrazujący agregację częściową:

public class Katalog
{
    private Dokument _swiadectwo;
    
    public void DodajSwiadectwo(Dokument swiadectwo)
    {
        _swiadectwo = swiadectwo;
    }
}

W powyższym przykładzie klasa Katalog posiada klasę Dokument, jednak instancja klasy Dokument jest utworzona na zewnątrz i przekazana do naszej klasy przez specjalną metodę.

Agregacja całkowita (kompozycja)

Agregacja całkowita (ang. composition) jest szczególnym rodzajem asocjacji. Dwie klasy w relacji agregacji całkowitej tworzą nierozerwalną jedność i nie mogą istnieć bez siebie niezależnie. Mają one także wspólny cykl życia.

agregacja-calkowita

Powyższy obrazek można czytać w następujący sposób: klasa System ma (na własność) klasę Plik. Pamiętaj, że nie chodzi tutaj zagnieżdżenie klas tylko o związek relacji całkowitej, czyli odpowiedzialność klasy głównej za istnienie klasy częściowej.

Oto kod obrazujący agregację całkowitą:

public class System
{
    private Plik _plik = new Plik();
}

Rzecz jasna, nie ma znaczenia w którym miejscu odbywa się tworzenie instancji klasy. Jeżeli klasa główna tworzy jakiekolwiek referencje wewnątrz, to zawsze będzie to relacja agregacji całkowitej.

public class System
{
    private Plik _plik;
    
    public System()
    {
        _plik = new Plik();
    }
}

Dziedziczenie

Dziedziczenie (ang. inheritance) jest głównym filarem paradygmatu programowania obiektowego. Dziedziczenie umożliwia wyodrębnienie cech wspólnych dla kilku klas i zamknięciu tych cech w klasie bardziej ogólnej - o wyższym poziomie abstrakcji.

Klasy dziedziczące po klasie bazowej przejmują jej cechy. Pozwala to znacznie skrócić kod i zorganizować kod od strony logicznej.

dziedziczenie-uml

Klasa Osoba posiada pewne cechy wspólne dla wszystkich klas, które ją rozszerzają. Przykładowy kod:

public class Osoba
{
    public string Imie { get; set; }
}

public class Student : Osoba
{
    public string NumerLegitymacji { get; set; }
}

public class Lekarz : Osoba
{
    public string NazwaSpecjalizacji { get; set; }
}

Klasy zagnieżdżone w UML

Na diagramie klas UML możemy umieścić także klasę zagnieżdżoną. Jej symbolem jest puste kółko z czarnym krzyżykiem.

zagniezdzenie-uml

Diagram należy czytać w następujący sposób: klasa Silnik jest zagnieżdżona w klasie Samochód. Przykładowy kod:

public class Samochod
{
    private string _marka;
    
    private class Silnik
    {
        private int _moc;
    }
}

Problemy ze zrozumieniem relacji UML

Kwestia nazewnictwa

Nazewnictwo relacji może być mylące, gdy będziemy chcieli przełożyć terminy angielskie na polskie odpowiedniki. Główny problem występuje z asocjacją i jej podtypami.

W luźnej dysku używając polskiego słowa “kompozycja” przeważnie mamy na myśli wszystkie rodzaje asocjacji. W modelowaniu obiektowym “kompozycja” jest głównym konkurentem dla mechanizmu dziedziczenia i generyczności. Mówiąc “kompozycja” raczej nikt nie myśli o agregacji całkowitej, a raczej o asocjacji, czyli jakiejś niedookreślonej relacji pomiędzy klasami, gdzie jedna używa drugiej.

Różnica pomiędzy asocjacją, a agregacją i kompozycją

Szczególnie wiele problemów sprawia dostrzeżenie różnicy pomiędzy asocjacją (ang. association), agregacją częściową (ang. aggregation) oraz agregacją całkowitą (ang. composition).

Niektórzy błędnie starają się opisać asocjację jako “słabszą”, “luźniejszą”, “mniej ważną” wersję kompozycji. Absolutnie nie powinniśmy tak ich definiować ani “umniejszać” priorytetów żadnej z nich.

Aby łatwo posługiwać się diagramami UML doradziłbym Ci skupić się na głównym podziale:

Powyższe zależności można zaprezentować kodem:

// zależność
public class Portfel
{
    public void  Wplac(Pieniadze pieniadze)
}

// asocjacja
public class Samochod
{
    private Silnik silnik;
}

// dziedziczenie
public class Rybka : Zwierze
{ }

W powyższym przykładzie zwróć szczególną uwagę na asocjację. Widzimy, że klasa Samochod ma klasę Silnik jednak nie wiemy, czy mogą one istnieć osobno, czy stanowią jeden nierozłączony byt. Jeżeli chcemy to doprecyzować, wtedy używamy agregacji częściowej lub całkowitej. W zdecydowanej większości przypadków takie szczegóły implementacji można pominąć się na diagramach UML.

Skoro agregacja częściowa i całkowita jest typem asocjacji, nasz podział powinien to uwzględnić i wyglądać następująco:

Kod może wyglądać następująco:

// asocjacja
public class Samochod
{
    private Silnik silnik;
}

// agregacja częściowa
public class Samochod
{
    public Silnik silnik;

    public Samochod(Silnik silnik)
    {
        _silnik = silnik;
    }
}

// agregacja całkowita
public class Samochod
{
    public Silnik silnik;

    public Samochod()
    {
        _silnik = new Silnik();
    }
}

Przykład: asocjacja we wzorcu adapter

W moim artykule opisującym wzorzec projektowy adapter widnieje następujący diagram UML dla jego wariantu obiektowego:

adapter-obiektowy-uml

Zwróć uwagę, że relacja pomiędzy adapterem a klasą adaptowaną jest określona jako asocjacja. Wynika to z faktu, że wzorzec adapter nie definiuje sposobu (siły relacji) przekazania obiektu adaptowanego do adaptera. Programista ma tutaj dowolność implementacji.

Klasa adaptera może przyjąć obiekt adaptowany jako parametr konstruktora, lub utworzyć instancję osobiście i być za nią odpowiedzialna. Z tego powodu jedynym słusznym oznaczeniem relacji na diagramie UML jest asocjacja, a nie agregacja.