Przeczytaj
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.
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.
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ć:
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.
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).
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:
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()
i przesun()
odpowiadają za zmianę kierunku poruszania się węża oraz za jego przesuwanie. Metody sprawdzPozycje()
i 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:
Klasa Pole
również musi zostać wpisana do diagramu pakietów. Uzupełniony pakiet „Zachowanie Rozgrywki” przyjąłby następującą postać:
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.
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:
Kompletny diagram, wraz z uzupełnionymi wartościami początkowymi tam, gdzie jest to uzasadnione.
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:
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.
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 klasyOwoc
tylko czasami, a nie co każdą klatkę animacji. Na diagramie oznaczylibyśmy to w następujący sposób:
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.
Jeden pracownik ma jedno stanowisko, na którym pracuje przez dłuższy czas, ale w każdym momencie może to stanowisko zmienić.
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:
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:
Do stosu brudnych naczyń wcale nie musi należeć widelec, ani talerz – sam StosBrudnychNaczyń może być pusty i czekać jedynie na uzupełnienie.
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.
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.
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:
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:
Teraz gdy już znasz wszystkie związki, zastanów się, do jakich klas powinniśmy podłączyć klasę Wyświetlanie Grafiki
i ObsługaWejścia
.
Związki od „najsłabszego” do „najsilniejszego” możemy pogrupować następująco:
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:
Wąż przesuwając się na dane pole, będzie ustawiał zmienną na wartość równą
- liczbaPunktów
.Poruszenie się węża będzie zwiększać wartość wszystkich ujemnych pól o jeden. Ostatecznie otrzymamy następujący schemat ruchu:
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:
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.
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.
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ów ER, które z kolei wykorzystywane są podczas budowania baz danych.
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
.
Cały nasz diagram obiektów mógłby przyjąć następującą formę:
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 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
inaczej diagram związków encji; diagram, dzięki któremu możemy zobrazować zależności w bazie danych