Kurs UML – część 1 – diagramy klas

Matthew @ 2010-09-12 — Kategorie: Inżynieria oprogramowania, Techblog, UML

Zmusiłem się (wreszcie) do napisania pierwszej (dość długiej, ze względu na obszerność materiału) części kursu UML. Jako „rzeczywisty” przykład obrałem moje fooaudio. Padła co prawda propozycja żeby oprzeć kurs o jakiś serwis webowy, ale jednak nie jestem w tej dziedzinie specjalistą. Jednak mimo tego, ktoś może się nie zgodzić z tym co mam zamiar tutaj przekazać. Nawet modelując ten sam projekt, z podobnym podejściem, dwóm różnym osobom może wyjść zupełnie inny diagram. Nie mówiąc już o późniejszej implementacji z tego diagramu.

Podstawą diagramu klas są oczywiście klasy, które przedstawia się jako prostokąt z nazwą klasy w środku: Pod polem z nazwą klasy, mogą się znajdować kolejne pola odpowiednio dla pól i metod (w UMLu nazywanymi odpowiednio atrybuty i operacje, aczkolwiek sam wolę nazewnictwo z programowania obiektowego, więc gdzieniegdzie mogę stosować je zamiennie). Jednak ich ilość może być dowolna (lub może nie być ich wcale, czyli zostanie sama nazwa klasy). Możesz tam umieścić jeszcze np. wyjątki zwracane przez metody klasy. Dla programistów Qt przydadzą się oddzielne pola na sygnały i sloty. Niestety, żadne narzędzie CASE z którego korzystałem nie udostępnia takiej opcji (albo ją bardzo zaszyli i nie mogę jej znaleźć). Ogólnie przyjmuje się że górne pole zajmuje nazwa klasy, poniżej są atrybuty a jeszcze niżej operacje:

Schemat opisywania atrybutów jest taki:

[cc no_cc=”true”]widoczność / nazwa : typ modyfikator [liczność] =  wartość_domyślna {właściwości i ograniczenia}[/cc]

Widoczność jest opisywana przy pomocy znaków [cc no_cc=”true” inline=”true”]- # ~ +[/cc], gdzie oznaczają one kolejno zasięg widoczności prywatny, chroniony, pakietu oraz publiczny. Następny jest slash (zwany ukośnikiem prawym czy ukośnikiem na Suwałki). Oznacza on atrybuty pochodne, które mogą być wyliczone z innych atrybutów. Atrybuty pochodne są przeważnie tylko do odczytu. Po ew. oznaczeniu atrybutu pochodnego podajemy nazwę atrybutu oraz typ. Nazwę od typu zawsze oddzielamy dwukropkiem. Typ możemy zmodyfikować przy pomocy takich modyfikatorów jak referencja ([cc no_cc=”true” inline=”true”]&[/cc]) czy wskaźnik ([cc no_cc=”true” inline=”true”]*[/cc]). Liczność argumentu wyrażamy natomiast w nawiasach kwadratowych umieszczonych zaraz po typie argumentu. Między nawiasami podajemy liczbę lub zakres. Zakres reprezentowany jest jako dwie liczby przedzielony dwoma kropkami ([cc no_cc=”true” inline=”true”][dolny..górny][/cc]). Najczęściej występujące formy zakresów:

  • [cc no_cc=”true” inline=”true”]0..1[/cc] – zero lub jeden,
  • [cc no_cc=”true” inline=”true”]0..*[/cc] – zero lub więcej (brak limitu),
  • [cc no_cc=”true” inline=”true”]1..*[/cc] – przynajmniej jeden element,
  • [cc no_cc=”true” inline=”true”]*[/cc] – dowolna ilość (w praktyce równoznaczny z zero lub więcej).

W następnej kolejności możemy podać domyślną wartość atrybutu poprzedzając ją znakiem równości. Ostatnią pozycją opisującą atrybuty są właściwości i ograniczenia, podajemy je na końcu, w nawiasach klamrowych:

  • [cc no_cc=”true” inline=”true”]ordered[/cc] – stosowany dla atrybutów, których liczność przekracza 1. Mówi o tym, że elementy powinny być posortowane,
  • [cc no_cc=”true” inline=”true”](not) unique[/cc] – również dla atrybutów o liczności powyżej 1. Elementy tego atrybutu (nie) muszą być unikalne (w ramach atrybutu),
  • [cc no_cc=”true” inline=”true”]readOnly[/cc] – nie wolno zmieniać początkowej wartości atrybutu. Nie jest jednak powiedziane kiedy ta wartość ma zostać nadana.

Oczywiście jest jeszcze więcej właściwości, ale są one przeważnie rzadko używane.

Ograniczenia mają schemat [cc no_cc=”true” inline=”true”]nazwa : ograniczenie[/cc], gdzie ograniczenie może być zapisane w języku naturalnym lub OCL. Można też podawać ograniczenia bez nazwy i dwukropka. A dla tych co rozwlekle piszą, zamiast opisywać ograniczenie wewnątrz diagramu klasy, można dołączyć do niego (a konkretnie do metody czy pola, które ma być nim objęte) notatkę z zapisanym ograniczeniem. Notatka (prostokąt z zagiętym rogiem) i jej dołączanie jest przedstawione na na rysunku wyżej. Notatki są chyba najlepszym featurem UMLa. Jak nie wiesz jak coś zrobić albo wyrazić, prawie na pewno możesz to zastąpić notatką. Atrybuty można oznaczyć jeszcze jako statyczne poprzez podkreślenie ich.

Operacje również mają swój schemat:

[cc no_cc=”true”]widoczność nazwa (parametry) : typ_zwracany {właściwości i ograniczenia}[/cc]

zaś parametry opisuje się w ten sposób:

[cc no_cc=”true”]kierunek nazwa : typ modyfikator [liczność] = wartość_domyślna {właściwości i ograniczenia}[/cc]

Większość elementów składki operacji ma takie samo (lub podobne) znaczenie jak w przypadku atrybutów, więc opisze tylko te, które mogą być niejasne. Kierunek określa to czy parametr służy nam do przekazywania danych do czy z metody:

  • [cc no_cc=”true” inline=”true”]in[/cc] – parametr jest przekazywany do operacji który go modyfikuje,
  • [cc no_cc=”true” inline=”true”]out[/cc] – parametr jest przekazywany z operacji na zewnątrz,
  • [cc no_cc=”true” inline=”true”]inout[/cc] – parametr jest najpierw do operacji, operacja modyfikuje go i jest tą samą drogą zwracany na zewnątrz,
  • [cc no_cc=”true” inline=”true”]return[/cc] – parametr jest najpierw przekazywany do operacji, operacja go modyfikuje i wraca poprzez return.

Pozostałe elementy składni parametrów mają takie same znaczenie jak w przypadku atrybutów. Jak widać operacje również są podobne w zapisie do atrybutów, widoczność znaczy to samo, nazwa też, po nazwie mamy właśnie parametry operacji podane w nawiasie (poszczególne rozdzielone są przecinkiem), następnie mamy dwukropek i tym zwracanej wartości przez operację. Większa różnica zaczyna się przy właściwościach i ograniczeniach. Opisuje się je podobnie jak właściwości i ograniczenia atrybutów. Przyjmuje się taki ich podział:

  • warunek wstępny – określa to w jakim stanie są atrybuty (lub klasa czy nawet cały system) dla danej operacji,
  • warunek ciała operacji – nakłada ograniczenia na tym zwracany,
  • warunek końcowy – określa stan w jakim powinny się znaleźć atrybuty, klasa czy system po wykonaniu operacji.

Przydatną właściwością operacji jest [cc no_cc=”true” inline=”true”]query[/cc]. Oznacza ona że operacja jest odpytująca. Znaczy to mniej więcej tyle, że operacja nie zmienia atrybutów klasy, chociaż dozwolone są zmiany w przypadku wewnętrznego cache obiektów.

Ostatnimi rzeczami, które są ważne z perspektywy operacji to oznaczanie ich jako statyczne i abstrakcyjne. Operacje statyczne oznacza się tak samo jak atrybuty statyczne, czyli poprzez podkreślenie operacji. Natomiast operacje oznacza się jako abstrakcyjne, poprzez zapisanie je kursywą. Natomiast, jeżeli chcemy aby nasza klasa była abstrakcyjna, postępujemy w podobny sposób, mianowicie pisząc nazwę klasy kursywą.

Kolejnym elementem diagramów klas są powiązania między klasami. Tutaj weźmy już jakiś bardziej rzeczywisty przykład (w wersji okrojonej, zawarcie wszystkiego byłoby za duże a i tak nie wniosła by nic nowego do tej lekcji):

Ważną informacja na wstępie: diagram nie jest przedstawieniem tego jak jest, ale jak powinno być. Sam kod pozwala na znacznie więcej, ale byłoby to sprzeczne z założeniami projektu.
Jako „podstawę” dla całej aplikacji mamy klasę FooApplication. Zawiera ona w sobie 3 elementy. FooAudioEngine (klasa silniku dźwięku), FooPlayerManager (klasa odpowiedzialna za sterowanie odtwarzaniem muzyki) i FooPlaylistManager (klasa odpowiedzialna za przechowywanie i manipulowanie playlistami). Klasy GUI zostały pominięte. Powiązania przedstawione na powyższym rysunku nazywają się kompozycjami. Przedstawia się je jako proste z zapełnionym rombem po stronie jednej z klas. Samo powiązanie przedstawia zależność całość – część, czyli jedna klasa jest częścią drugiej (tej po stronie której jest romb). Ma to swoje konsekwencje takie, że obiekty klasy „części” nie mogą występować samodzielnie pod żadną postacią nigdzie indziej. Chociaż sama klasa „części” może być „częścią” dla wielu innych klas. Wiąże to również obiekty klas pod względem czasu życia. Obiekty „części” przeważnie zawsze umierają razem z obiektem klasy „całości”. Wyjątkiem jest sytuacja, w której przed zniszczeniem klasy zawierającej przepiszmy obiekty do innej instancji klasy, która również zawiera klasę „części”. Jak to się ma przykładu? Mimo że FooApplication nie wpływa znacząco na pozostałe klasy, to jednak bez klasy aplikacji nie powinny one istnieć. Jeżeli jedna klasa nadal ma być częścią innej, jednak bez tak silnej zależności życia lub z możliwością istnienia obiektu klasy gdzieś indziej, wybieramy agregację. Reprezentujemy ją poprzez linię z „pustym” rombem przy klasie która zawiera klasę związaną z nią tym powiązaniem. Jak później pokażę, obiekty FooTrack mogą istnieć bez playlisty. Taka sytuacja zachodzi, gdy ktoś usunął aktualnie graną piosenkę z playlisty a w jakiś sposób musimy wyświetlić o niej informacje. Kolejnym rodzajem powiązania jest asocjacja (prosta linia, bez dodatków). Czasy życia obiektów tych klas nie są ze sobą związane (jedną trwają przez pewien czas), co oznacza że jedna może zostać zniszczona bez konsekwencji dla drugiej (co akurat w tym przypadku nie jest do końca prawdą, bo zniszczenie instancji FooPlaylistManager, przy późniejszej próbie uzyskania informacji przez FooPlayerManager spowoduje wysypanie się programu. Jednak z powodu pewnych założeń w samym programie można przyjąć że to będzie działało prawidłowo ;]). Asocjacją oznaczamy sytuację, w której jedna klasa w jakiś sposób wpływa na drugą. Tutaj zachodzi to w przypadku, gdy FooPlayerManager chce przekazać następną piosenkę do odegrania silnikowi, więc musi ją pobrać z aktualnej playlisty.

Następnym powiązaniem, któremu się przyjrzymy jest zależność. Czas życia obiektów nie jest praktycznie ze sobą w żaden sposób powiązany a interakcje między klasami są przeważnie krótkotrwałe.  W przykładzie dochodzi do niego, gdy silnik chce uzupełnić metadane piosenek. Dobrym przykładem zależności są również klasy eventowe przekazywane przez metody, które są wywoływane właśnie przy jakiś zdarzeniu. I tutaj od razu przykład do czego można wykorzystać UMLowy szwajcarski scyzoryk, zwany potocznie notatką. Mianowicie do opisania powiązania. Zależność przedstawiamy jako przerywaną linię z grotem skierowanym na klasę wykorzystywaną. Ostatnim rodzajem powiązania jest generalizacja. Służy ona do wyciągania wspólnych cech różnych klas. Reprezentujemy ją poprzez ciągłą linię z pustą strzałką przy klasie bazowej. Aby nie pomylić się przy rysowaniu tej strzałki, należy to powiązanie czytać jako X to Y, np. Kot to Zwierzę. W programowaniu obiektowym generalizacja odpowiada dziedziczeniu.. Dopuszczane jest również wielodziedziczenie. W fooaudio głównym zastosowaniem generalizacji jest możliwość dostarczenia obsługi jednego zadania (tutaj grania muzyki przez silnik) na wiele różnych sposobów. Wiąże się to też z mechanizmem pluginów.

Większość powiązań (oprócz generalizacji) dopuszcza określania liczności między klasami. Schemat jest taki sam jak w przypadku liczności atrybutów, tylko że podajemy je obok linii reprezentującej powiązanie przy klasie której liczność ma dotyczyć. Domyślna liczność to 1, więc można ją pominąć. Na powyższym schemacie (idąc od lewej) mamy FooPlaylistManager, który zawiera przynajmniej jedną playlistę, ale jedna playlista może być częścią tylko jednego menedżera playlist. Podobnie jest w przypadku playlist i tracków, tylko w tym przypadku, playlista może być pusta.

Asocjacjom można również naadć inne właściwości. Pierwsza to nazwa asocjacji. Raczej nie ma wielkiego odwzorowania w kodzie, służy głównie do modelowania i przedstawiania działań jednej klasy na drugiej. Reprezentuje się je poprzez nazwę akcji oraz jednolity grot strzałki wskazujący kierunek czytania asocjacji. Kolejną właściwością jest komunikacyjność (na rysunku między FooPlayerManager a FooTrack). Sama strzałka mówi nam o tym w którą stronę może zachodzi komunikacja (jeżeli komunikacja zachodzi w obie strony strzałki się pomija). Jeżeli chcemy zabronić komunikacji jednej klasie z drugą, to należy umieścić X na linii asocjacji przy klasie z którą nie można się komunikować. Tyle jeżeli chodzi o fooaudio, reszta będzie przedstawiana na bardziej abstrakcyjnych przykładach. Powiązania między klasami możemy również przedstawić jako atrybuty. Schemat nazw jest podobny do tego jaki nadajemy normalnym atrybutom, tylko że piszemy go przy linii oznaczającą powiązanie. Jeżeli mamy taką potrzebę to można z tym zrobić niezłe sztuczki: Ten schemat mówi nam o tym, że Samochód może mieć albo manualną skrzynię biegów albo automat. Jednak nigdy te dwie na raz, dzieje się tak poprzez wykorzystanie xora, który wyklucza drugi atrybut w przypadku wybrania jednego z nich. OK, ale skoro powiązania mogą być atrybutami, to po co stosować te drugie (lub na odwrót)? W zasadzie to jest wymienne. Albo zależy od sytuacji. Dla typów prostych piszę atrybuty w klasie, dla złożonych które sam stworzyłem w ramach projektu wykorzystuję powiązania. Chociaż możecie się też spotkać z opinią, że powiązania są zarezerwowane dla diagramów analitycznych a atrybuty dla projektowych.

W przypadku asocjacji możemy mieć również do czynienia z klasami asocjacyjnymi. Reprezentujemy je jak normalne klasy tylko zamiast powiązania z innymi klasami, wiążemy je linią przerywaną z asocjacją której ma dotyczyć. Tworzymy je, gdy chcemy zamodelować bardziej złożony związek między klasami. Powyższy przykład jest mocno średni, ale nie udało mi się wymyślić nic bardziej sensownego. I tak mamy jakąś osobę, która mieszka na (czyli jest powiązana) jakiejś ulicy, jednak musi również mieszkać w jakimś domu na tym ulicy. Od strony implementacji nie jest wymagane, aby klasa Osoba była bezpośrednio powiązana z Ulicą, wystarczy jeżeli będzie powiązana z Domem, a Dom z Ulicą.

Ważnym elementem OOP są interfejsy. W UMLu realizuje się je poprzez nadanie klasie stereotypu [cc no_cc=”true” inline=”true”]<<Interface>>[/cc]. Same stereotypy mogą nam również posłużyć do oznaczania enumów czy singletonów (i wielu innych rzeczy). Modelowanie realizacji interfejsu jest podobna do generalizacji, z tą różnicą, że linia reprezentująca realizację nie jest ciągła a przerywana. Również klasa realizująca interfejs musi implementować metody tego interfejsu. Natomiast klasy chcące wykorzystać właściwości jakie daje realizacja interfejsu powinny się odnosić do interfejsu (jak na przykładzie powyżej) a nie do poszczególnych klas które go realizują.

UML pozwala również na określanie szablonów/wzorców. Jeżeli chcemy aby nasza klasa zawierała szablon należy dorysować jej, na prawym górnym rogu, przerywaną linią, prostokąt zawierający listę parametrów szablonu. Pierwszy (T) jest parametrem ogólnym, a drugi (U) jest parametrem z ograniczeniem wskazującym na to, że może przyjmować tylko klasy będące pochodnymi Sortowalny. Poprzez generalizację możemy utworzyć nową klasę, już z określoną listą parametrów. Zabieg ten nazywa się wiązaniem. Służy do tego słowo kluczowe [cc no_cc=”true” inline=”true”]<<bind>>[/cc] umieszczone przy generalizacji. W nawiasach trójkątnych podajemy wiązania wg. schematu: [cc no_cc=”true” inline=”true”]Nazwa parametru -> Klasa[/cc].

To tyle. Jeżeli coś pominąłem, a wydaje się to komuś być ważne to niech napisze w komentarzach a ja dopiszę. Wszelkie błędy również można zgłaszać w komentarzach. Następna część (chyba że nikt jej nie chce) będzie o use case’ach.

Komentarze:

Bardzo się ciesze, że napisałeś artykuł na przykładzie programu, też jestem programistą[C++/Qt4/AS3] więc łatwiej mi było zrozumieć :]

Dopiero zaczynam z UML’em bo przy projekcie > ~15 Klas, już robi się trochę trudno bez żadnego schematu.
Szkoda, że na koniec nie zamieściłeś bardziej rozbudowanego schematu, razem z GUI.

Według mnie to jeden z lepszych kursów UML’a w sieci pl, najważniejsze w kursach są przykłady ;].

drobne błędy:
-Asocjacjom można również inne właściwości.
-ale nie udami mi się wymyślić nic bardziej sensownego.
+Asocjacjom można również nadać/dodać/przypisać inne właściwości.
+ale nie udało[imho powinno być w czasie przeszłym] mi się wymyślić nic bardziej sensownego.

@skunet1248: problemem ze schematem z pełnym schematem z GUI jest to, że jest zbyt obszerny, żeby go zamieścić tutaj. Można by dać jakiś PDF. Ale prawdę mówiąc nigdy nie stworzyłem takiego schematu… znaczy nie stworzyłem go sam, bo na uczelni robiliśmy takie rzeczy, a sam do fooaudio schemat jest tylko rysunkami klas. Problem w tym, że nie znam narzędzia które by mi pozwoliło na wyciągnięcie sygnałów i slotów w kolejne pola. Druga rzecz, że fooaudio raczej sam rozwijam a nie mam też specjalnych problemów z zapamiętaniem co do czego służyło. Chociaż pewnie przy wydaniu kolejnej wersji jakąś dokumentację deweloperską będę musiał zrobić.

Dzięki za zgłoszenie błędów. Już poprawiłem.

Mnie tesz ciekavi UML. A szczegulnie jakim programem najvygodniej było by rysovać te diagramy. Prubovałem Umbrello i Dia. Ale piervszy ma błędy, a drugi jest toporny. Natomiast ten co Ty używasz robi ładne diagramy. Co to za program???

bobas FAIL ;/

@Nowaker
Co masz na myśli tym FAIL???? Pszecież programu tak ńkt by ńe nazvał???

Abo to ja musze znać vszystkie programy do UML??? Co v tym dźvnego, że pytam jak człoviek? To może ktoś jak człoviek odpovie?

@bobas: im (przynajmniej Nowakerowi) nie chodzi o niewiedzę w nazwach programów (chociaż zawsze się dziwię że ludzie nie czytają od początku albo do końca), ale o poprawność językową komentarzy („tesz” zamiast też, „szczegulnie” zamiast szczególnie, „prubowałem” zamiast próbowałem i notoryczne wstawianie „v” zamiast w).

No tak, zapomniałem dodać linka:
[skasowany link]

@bobas: wybacz, ale musiałem wywalić linka. Krótko: nie życzę sobie reklamowania na moim blogu jakiś bzdur. I wymagam chociaż trochę poprawnej polszczyzny (nie musi być perfekcyjna, ale ma trzymać poziom). Masz się do tego dostosować.

To jest jakiś troll, upatrzył sobie Ciebie za ofiarę, a ze wszystkich ma niezłą bekę. ;-]

@Nowaker
Akurat!!! Chciałbym zauważyć, że autor tego bloga ma ambicje zostać trolem! Pisze o tym w sekcji „O mnie”. Dodam tylko, że takim trolem co robi Qt. Oby mu się udało!!! Bo polacy pracują w Qt (np. w sprawie zakupu licencji można się dogadać z nimi po polsku).

Jednym z lepszych programów do diagramów jest Enterprise Architect i wygląda mi, że to w nim autor tworzył diagramy klas. Program jest niestety komercyjny i płatny.

bobas sprawdz StarUML <- wymiata dla poczatkujacych !

Grupa programów astah wśród nich jest darmowy astah community też daje rade

Bartłomiej @ 10 lutego 2012 o 11:20:

Hej, dzięki za zwięzłe wprowadzenie a w zasadzie przypomnienie wiadomości. Komentarz piszę głównie po to by zachęcić Cię do budowania kolejnych rozdziałów w podobnej formie.

Pozdrawiam
b

Nie znam się na tym w ogóle, ale mam pytanie czy potrzebna jest znajomość Javy, żeby tworzyć projekty w UML ? Będę miał przedmiot na którym będziemy pracować w UML ale ja nic o nim nie wiem i zastanawiam mnie czy bez znajomości Javy można jakoś działać ?

@Gosc: nie wiem skąd przyszedł Ci pomysł z Javą. Fajnie by było jakbyś znał trochę podstaw paradygmatu programowania obiektowego, ale niekoniecznie na konkretnym języku. UML stoi wyżej od Javy (czy jakiegokolwiek innego języku programowania). Służy do projektowania rozwiązań a języki programowania (w tym Java) do ich implementacji.

Zawile i nic nie skumałem. Właściwie skumałem lecz tylko dlatego że mam w tym kilka lat doświadczenia. Tak jakby autor poszedł na skróty. Sztuka dal sztuki. UML jest językiem abstrakcyjnym choć opisem nawiązuje do składni obiektowego C. Odradzam korzystanie z dostępnych w sieci kursów. Bez zrozumienia programowania obiektowego na poziomie inżynierskim zabieranie się za notację UML jest pozbawione sensu. I bynajmniej dyplom inżyniera z jakieś uczelni nie gwarantuje zrozumienia czy też umiejętność programowania w jakimś języku.

kiedy nastepne czesci kursu ?

Pingback: Projektowanie gry | Szymon Siarkiewicz

Dodaj komentarz:

 

Subscribe without commenting