Diagram klas

Diagram klas powinien być tworzony na podstawie diagramu pakietów lub – co najmniej – powinien być z nim kompatybilny.

Przypomnijmy, jak wyglądał diagram z poprzedniego e‑materiału i na jego podstawie spróbujmy opracować następny diagram.

R12TkaS9hZi0G
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Zacznijmy od pakietu sterowania. Znajduje się w nim jedna klasa. Zastanówmy się, jakie metody powinniśmy zaimplementować.

W klasie koniecznie musi się znaleźć metoda, która będzie poprawnie przechwytywać komunikaty gracza i prawidłowo je przetwarzać. W tym miejscu musimy zwrócić uwagę, że dla Javy, C++ i Pythona wyświetlanie może działać troszeczkę inaczej. Dlatego właściwa implementacja może wymagać użycia klas charakterystycznych dla danego języka. Przy czym nasz diagram pakietów jest ogólny dla kilku języków.

W naszym przypadku postaramy się stworzyć jak najogólniejszy diagram – tak, aby nie kierować toku myślenia na jakiś konkretny język.

Przykład 1

Dziedziczenia w językach Java, C++ i Python mają różne ograniczenia. W C++ i Pythonie dziedziczenie po dziesięciu klasach jednocześnie jest możliwe, w Javie już nie. Wynika to z tego, że języki C++ i Python umożliwiają tzw. wielodziedziczenie.

Sama klasa, w diagramie klas, będzie przybierać następującą postać:

RNUoy8xWhUB9D
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Plus przed nazwą oznacza, że dana metoda lub zmienna jest publiczna (public), minus przed nazwą oznacza, że dana metoda lub zmienna jest prywatna (private), a hash przed zmienną lub metodą oznacza ochronę (protected).

W naszej klasie ObsługaWejścia musimy zawrzeć metodę, która będzie odpowiednio modyfikować stan rozgrywki, w zależności od tego, co gracz wprowadzi na wejściu.

RT42jz2VIrJqu
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Klasa ta, pomimo posiadania wyłącznie jednej metody i tak będzie dość rozbudowana. Musimy w niej zawrzeć interpretację dla możliwości klikania trzech przycisków w trakcie rozgrywki. Natomiast w momencie, gdy rozgrywka jest zatrzymana, musimy również poprawnie zinterpretować to, co gracz chciał nam przekazać.

Kolejną klasą, którą stworzymy, będzie Owoc. Klasa ta będzie posiadała przynajmniej jedną zmienną przechowującą informację, jaką liczbę punktów otrzyma gracz, jeżeli dany Owoc zostanie zjedzony. Zawrzemy w niej również metodę sprawdzPunkty(), ponieważ odczytywanie bezpośrednio ze zmiennej zawartej w samej klasie niesie ze sobą kilka problemów.

Po pierwsze, odczytywana jest wartość oryginalna i niezmodyfikowana, a istnieje spora szansa, że w toku tworzenia oprogramowania stwierdzimy, że potrzebne będzie wstępne przetworzenie tej wartości przed jej użyciem. Dlatego też zmienne z tej klasy będziemy odczytywać za pomocą getterów, a modyfikować za pomocą setterów. Same zmienne będą natomiast prywatne, żeby uniknąć pomyłek w trakcie pisania kodu. Ukrywanie zmiennych i metod jest określane w programowaniu obiektowym jako hermetyzacja (czyli enkapsulacja lub kapsułkowanie).

R15HdfsaiE12Y
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Klasa Wąż będzie już bardziej rozbudowana. Potrzebujemy kilku informacji. Po pierwsze, informację o punktach będziemy przechowywać jako jego zmienną. Z racji tego, że liczba punktów determinuje długość ogona, dla oszczędności pamięci zdefiniujemy tę zmienną w klasie Wąż. Oczywiście można taką zmienną umieścić również w innym miejscu, np. w klasie Plansza lub nawet teoretycznie można zdefiniować w tym celu osobną klasę. To ostatnie rozwiązanie w tym przypadku nie ma sensu, ponieważ klasa taka przechowywałaby tylko jedną zmienną. Prezentowane rozwiązanie jest prostsze i czytelniejsze.

Warto zaznaczyć, że wracanie do poprzednich diagramów jest jak najbardziej akceptowalną praktyką. Teraz jednak postaramy się trzymać diagramów wcześniej ustalonych.

Dla zmiennych klasy Wąż również skorzystamy z getterów i setterów. Za ich pomocą będziemy pobierać i ustawiać wartość zmiennych zawierających informacje o położeniu głowy węża. Inną ważną zmienną jest zmienna przechowująca liczbę punktów. Wąż powinien bowiem przechowywać także informację o położeniu swojego ogona.

W tym miejscu dochodzimy do ważnego pytania, na które musimy jednoznacznie odpowiedzieć. W jaki sposób wykryjemy kolizje zachodzące pomiędzy głową a ogonem?

Zrobimy to tak, że wąż będzie sprawdzał, czy dany punkt na planszy, na który chce się przemieścić, zawiera ogon. Jeśli zawiera, to wtedy gracz przegrywa i wyświetla się wynik punktowy. Jeśli jednak nie, to gra toczy się dalej. Dlatego też informacja o tym, czy dane pole zawiera ogon czy też go nie zawiera, będzie przechowywana w ramach następnej klasy – Plansza.

Wróćmy jednak do klasy Wąż.

Ta, ostatecznie, będzie wyglądać w następujący sposób:

R1bhKJ7hv06WZ
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Będzie w niej zdefiniowanych siedem metod. Metoda sprawdzWynik() zwraca aktualny wynik punktowy gracza. Metoda ustawWynik() ma za zadanie ustawienie aktualnego wyniku na określoną wartość. Metody zmienKierunek()przesun() odpowiadają za zmianę kierunku poruszania się węża oraz za jego przesuwanie. Metody sprawdzPozycje()ustawPozycje() posłużą odpowiednio do uzyskania informacji o aktualnej pozycji węża oraz do ustawiania tej pozycji. Metoda zjedzOwoc() będzie wywoływana w momencie, gdy wąż natrafi na umieszczony na planszy owoc.

Kolejną, wcześniej wspomnianą klasą, będzie klasa Plansza. W niej musimy zdefiniować samą wielkość planszy oraz informację o tym, czy na danym polu znajduje się owoc, ogon, czy pusta plansza.

Już na etapie projektowym natrafiliśmy na problem dotyczący tego, w jaki sposób będą definiowane pola planszy. Czy klasa Plansza powinna składać się z tablicy dwuwymiarowej, czy może jednak być pewnego rodzaju listą?

Najprościej jest utworzyć kolejną klasę o nazwie Pole. W polu tym zostanie zdefiniowana zmienna, której wartością będzie obiekt klasy Owoc lub null. Wynikiem naszych rozważań są dwie klasy:

RDJZhtH0JD39r
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Klasa Pole również musi zostać wpisana do diagramu pakietów. Uzupełniony pakiet „Zachowanie Rozgrywki” przyjąłby następującą postać:

RSS0E4Bzyh1wA
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Ostatnią klasą, którą będziemy omawiać, jest klasa o nazwie Wyświetlanie Grafiki. Klasa ta korzystać będzie z informacji, które otrzyma z klasy Plansza. Nie będzie ona miała rozbudowanego projektu, ponieważ samo działanie tej klasy nie jest złożone. Odpowiednia metoda przyjmie za argument klasę Pole i klasę Wąż, a potem na ich podstawie wygeneruje obraz.

R1Ez740LQXNaq
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

W diagramie możemy także umieszczać informację o typie zmiennej.

Przykład, który agreguje wszystkie opisane wcześniej klasy w jeden diagram wraz z typami danych:

R6q2aP3rjaESJ
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Kompletny diagram, wraz z uzupełnionymi wartościami początkowymi tam, gdzie jest to uzasadnione.

R4erBYpXojo9Y1
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

W klasie Wąż została zdefiniowana zmienna, która jest obiektem klasy Point. W założeniu chodziło tutaj o klasę, która służy do oznaczania punktów w przestrzeni dwuwymiarowej. Jednak aby uprościć i uogólnić nasz diagram na wszystkie języki programowania, skorzystamy z takiej wersji:

R8p1yIxmJ7rdd
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Dla zwiększenia czytelności zaznaczymy jeszcze związki pomiędzy klasami. Najpierw przejdźmy do omówienia poszczególnych związków. Dostępnych jest 5 różnych związków.

  1. Przerywana strzałka oznacza zależność. Używa się jej w momencie, gdy obiekty jednej klasy korzystają z obiektów drugiej klasy w sposób nieciągły. W naszym przypadku klasa Wąż będzie korzystać z obiektów klasy Owoc tylko czasami, a nie co każdą klatkę animacji. Na diagramie oznaczylibyśmy to w następujący sposób:

Rjhp7pQejv9ak
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.
  1. Pojedyncza linia, która oznacza asocjację, czyli sytuację, gdy obiekt danej klasy wykorzystuje obiekty innej klasy nie ciągle, ale przez dłuższy czas. W naszym diagramie nie ma takiej sytuacji, więc posłużymy się przykładem spoza niego.

R1XEuttoH1eav
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Jeden pracownik ma jedno stanowisko, na którym pracuje przez dłuższy czas, ale w każdym momencie może to stanowisko zmienić.

  1. Linia zakończona pustym rombem oznacza agregację częściową. W naszym diagramie również nie ma dobrego przykładu obrazującego taką zależność. Dlatego też skorzystamy z przykładu spoza naszego diagramu klas:

RhhllEfwBrzIr
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Agregacja ta polega na tym, że element częściowy nie jest zależny od elementu głównego – element częściowy może bez problemu istnieć bez elementu głównego, a jeden element częściowy może należeć do wielu elementów głównych.

Talerz i widelec istnieją bez zmywarki, a zmywarka wcale nie potrzebuje do działania talerza i widelca. Analogicznie możemy dodać do tego:

R1NsopK6Uwciy
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Do stosu brudnych naczyń wcale nie musi należeć widelec, ani talerz – sam StosBrudnychNaczyń może być pusty i czekać jedynie na uzupełnienie.

  1. Linia zakończona zamalowanym rombem to agregacja całkowita, nazywana czasami kompozycją. Jest podobna do agregacji częściowej. Ten typ agregacji charakteryzuje się tym, że istnienie klasy częściowej zależy od istnienia klasy głównej.

R3etQjWWXpKmw
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Na naszym diagramie zaznaczylibyśmy to w ten sposób – klasa Owoc może istnieć tylko w ramach klasy Pole i nigdy nie może istnieć bez niej. Analogicznie klasa Pole występuje jedynie w ramach klasy Plansza i nigdy nie będzie występowała samodzielnie.

  1. Ostatnim rodzajem związku jest pusta strzałka, która oznacza dziedziczenie. To dokładnie to samo, co dziedziczenie w językach programowania. Na naszym diagramie nie znajdziemy takich przypadków, jednakże możemy posłużyć się dwoma przykładami:

R1LU9rjoz3GAK
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Pies dziedziczy po Ssaku, który dziedziczy po Zwierzęciu. W ten sposób każdy Pies otrzymuje cechy Ssaka. Każdy Ssak z kolei otrzymuje cechy zwierzęcia.

Uczeń Liceum dziedziczy po Nastolatku, otrzymując jego właściwości, analogicznie Nastolatek dziedziczy po Człowieku jakiś zbiór zdolności.

Po wprowadzeniu wszystkich zależności, nasz diagram klas wygląda następująco:

R1QSmOMBI0eJf
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.
Polecenie 1

Teraz gdy już znasz wszystkie związki, zastanów się, do jakich klas powinniśmy podłączyć klasę Wyświetlanie GrafikiObsługaWejścia.

Ważne!

Związki od „najsłabszego” do „najsilniejszego” możemy pogrupować następująco:

RvWkioj1OtJ8P
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Czy to już wszystkie potrzebne nam metody? Niekoniecznie. Spójrzmy na diagram klas i zastanówmy się, skąd będziemy wiedzieć, gdzie jest ogon węża.

Odpowiedź brzmi – nie będziemy wiedzieć. Z samej klasy Wąż jesteśmy w stanie wywnioskować obecną pozycję głowy węża. Możemy teoretycznie zapamiętywać poprzednie pozycje i na tej podstawie dalej dokonywać obliczeń, jednakże nie jest to coś, co uwzględniliśmy na samym diagramie.

Najprościej będzie podjąć następujące kroki:

  1. Wąż przesuwając się na dane pole, będzie ustawiał zmienną na wartość równą - liczbaPunktów.

  2. Poruszenie się węża będzie zwiększać wartość wszystkich ujemnych pól o jeden. Ostatecznie otrzymamy następujący schemat ruchu:

R1g5cKt0zkEOG

0

0

0

0

0

0

-1

-2

0

0

0

0

-3

0

0

0

0

-4

-5

0

0

0

0

0

0

0

0

0

0

0

Pole zaznaczone na żółto oznacza głowę. Wartość -5 to obecna liczba punktów na minusie, a kolejne ujemne wartości to ogon.

W kolejnym ruchu plansza wyglądałaby następująco:

R143tfXXNzmvE

0

0

0

0

0

0

0

-1

0

0

0

0

-2

0

0

0

0

-3

-4

-5

0

0

0

0

0

0

0

0

0

0

Zatem każdy element Planszy, czyli każda klasa Pole, powinny mieć dodatkową zmienną, która będzie służyła do śledzenia wartości na danym polu.

Oznacza to również, że musimy poprawić nasz schemat poprzez dodanie odpowiedniego związku pomiędzy klasą Wąż, a klasą Plansza.

Pamiętajmy, że nie ma żadnej zmiennej odpowiedzialnej za kierunek ruchu węża, która powinna być obecna w klasie Wąż. Dlatego umieśćmy ją jako atrybut w ramach klasy Wąż.

W dodanym związku elementem głównym jest Plansza, ponieważ to ona – na podstawie informacji dostarczanych przez klasę Wąż – modyfikuje swoje zmienne.

R1EPJM1z6PZia
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Poprzez ciągłe dodawanie elementów do naszego diagramu chcemy pokazać, że jest to proces, który w przypadku bardziej skomplikowanych projektów może trwać całymi tygodniami. W trakcie implementacji zawsze jednak może dojść do sytuacji, gdy będziemy potrzebować jakiejś dodatkowej zmiennej albo kolejnej metody. I jest to, oczywiście, normalna sytuacja. Nie należy jednak zakładać, że takie przypadki z góry dopuszczamy i się nimi nie przejmujemy, ponieważ wtedy sens robienia diagramu klas się zatraca.

Diagram obiektów

Diagram ten jest podobny do diagramu klas, stworzymy go więc na jego podstawie. Diagram ten pokazuje kilka przykładowych obiektów stworzonych z naszych klas, a także sposób, w jaki są one ze sobą powiązane.

RzwYWG7eM4g6e1
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Na diagramie tym umieszczamy konkretne przykładowe wartości, którymi w prosty sposób możemy zobrazować, jak będzie zachowywał się nasz program.

Dodatkową informacją, jaką możemy umieścić na diagramie obiektów, jest informacja o krotnościach związków, które możemy znać z diagramów ERdiagram ERdiagramów ER, które z kolei wykorzystywane są podczas budowania baz danych.

Rip39I73fv8rP
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Tak zapisana informacja oznacza, że każdemu Polu odpowiada jeden i tylko jeden Owoc. Tak samo możemy odczytać, że każdemu Owocowi odpowiada jedno i tylko jedno Pole. Pozostaje więc pytanie, która jedynka tyczy się której informacji?

Zasada jest taka, że patrzymy „z perspektywy” danej klasy, na klasę z nią połączoną i ta liczba „bliżej nas” oznacza, ile tych drugich klas będzie połączonych z naszą klasą.

Dlatego też patrząc z perspektywy obiektu Plansza, widzimy, że będziemy mieli z nim połączonych n obiektów klasy Pole.

Rx8fAM4DPb080
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Cały nasz diagram obiektów mógłby przyjąć następującą formę:

RFlhVpmjlB4Sw
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Warto zaznaczyć, że nie jest to jedyna możliwa poprawna forma. Chcieliśmy pokazać użycie klas poprzez stworzenie przykładowych obiektów i to właśnie zrobiliśmy.

Słownik

architekt oprogramowania
architekt oprogramowania

architekt odpowiadający za zaplanowanie struktury programu oraz za zależności w niej występujące; w większych projektach to on jest odpowiedzialny za obmyślanie wcześniej wspomnianych elementów

diagram ER
diagram ER

inaczej diagram związków encji; diagram, dzięki któremu możemy zobrazować zależności w bazie danych