Celem laboratorium jest zapoznanie się z różnymi metodami usprawniania kodu (refactoringu) bez wprowadzania nowych funkcji. Wprowadzimy mechanizm wyjątków, a także zapoznamy się z przydatnymi wzorcami projektowymi, takimi jak metoda szablonowa czy obserwator.
Najważniejsze zadania:
- Walidacja danych przy użyciu mechanizmu wyjątków.
- Zastosowanie wzorca
template method
do wyliczania granic mapy. - Zastosowanie rekordów do tworzenia kontenerów na dane.
- Zastosowanie wzorca
Observer
.
- W metodzie odpowiedzialnej za zamianę argumentów aplikacji na ruchy zwierzęcia rzuć wyjątek
IllegalArgumentException
, jeśli którykolwiek z parametrów nie należy do listy poprawnych parametrów (f
,forward
,b
,backward
, etc.). Jako przyczynę wyjątku wprowadź łańcuch znaków informujący, że określony parametr jest niepoprawny, np.new IllegalArgumentException(argument + " is not legal move specification")
. - Stwórz własną klasę wyjątku -
IncorrectPositionException
. Powinien być to wyjątek typu checked. Wyjątek powinien przyjmować w konstruktorzeVector2d
i tworzyć na jego podstawie wiadomość np.Position (x, y) is not correct
. - W metodach odpowiedzialnych za dodawanie elementów do mapy, jeśli dodanie elementu na wybrane pole jest niemożliwe, rzuć wyjątek
IncorrectPositionException
. Wyjątek zastępuje sygnalizowanie błędu przy pomocy zwracania wartościfalse
(zmień sygnaturę metody). - Obsłuż oba wyjątki. W przypadku błędów walidacji opcji program powinien zostać przerwany z odpowiednim komunikatem. W przypadku próby ustawiania zwierzątek na złych pozycjach w klasie
Simulation
powinny one być pominięte, ale program nadal ma działać i umożliwiać symulację dla poprawnie ustawionych zwierzątek. - Zaktualizuj testy metody
place
oraz klasyOptionsParser
, aby były zgodne z nowym kontraktem.
- Stwórz rekord
Boundary
, który będzie przechowywał dwie pozycjeVector2d
- lewy dolny róg i prawy górny róg (opisujące prostokątny obszar). - Dodaj do klasy
AbstractWorldMap
abstrakcyjną metodęgetCurrentBounds()
, która będzie zwracała obiektBoundary
. Uwaga: możesz dodać tę metodę także do interfejsuWorldMap
. - Zaimplementuj metodę w obu realizacjach mapy, korzystając z istniejącego już kodu.
- Pozbądź się z obu realizacji mapy metody
toString()
oraz atrybutuMapVisualizer
- przenieś je do klasy bazowej. W tym przypadkutoString()
powinno stać się metodą szablonową. Wykorzystaj w tym celu stworzoną wcześniej metodęgetCurrentBounds()
.
-
Stwórz nowy interfejs
MapChangeListener
, zawierający jedną metodę:void mapChanged(WorldMap worldMap, String message)
. -
Klasa
AbstractWorldMap
będzie naszym typem obserwowanym (observable). Dodaj do niej niezbędne elementy zgodnie ze wzorcem:- Klasa powinna przechowywać listę swoich obserwatorów realizujących interfejs
MapChangeListener
. - Klasa powinna umożliwiać rejestrowanie i wyrejestrowywanie obserwatorów.
- Umieszczenie zwierzęcia na mapie lub jego poruszenie powinno skutkować powiadomieniem wszystkich obserwatorów z podaniem opisu, co się wydarzyło (stwórz dodatkową metodę pomocniczą np.
mapChanged(String)
- powinna wywoływać metodę z interfejsuMapChangeListener
na wszystkich zarejestrowanych obserwatorach).
- Klasa powinna przechowywać listę swoich obserwatorów realizujących interfejs
-
Stwórz klasę
ConsoleMapDisplay
- to będzie nasz pierwszy obserwator (observer). Kolejnych dodamy na późniejszych zajęciach. Klasa powinna:- realizować interfejs
MapChangeListener
, - w reakcji na zmianę mapy wypisywać kolejno:
- otrzymaną informację o operacji wykonanej na mapie,
- wizualną reprezentację otrzymanej mapy (
toString()
), - sumaryczną liczbę wszystkich otrzymanych do tej pory aktualizacji (zdefiniuj odpowiedni atrybut).
- realizować interfejs
-
Zarejestruj
ConsoleMapDisplay
jako obserwatora dla tworzonej mapy - możesz to zrobić np. w klasieWorld
. -
Pozbądź się wypisywania stanu mapy z klasy
Simulation
. Jeśli wszystko poszło ok, mapa i tak będzie się wypisywać po każdej zmianie pozycji!Zastanów się, co nam daje takie rozwiązanie. W jaki sposób zastosowanie wzorca obserwator może wpłynąć korzystnie na dalsze rozwijanie naszego kodu?
-
Wyjątki są mechanizmem pozwalającym przekazywać informację o błędzie pomiędzy odległymi fragmentami kodu.
-
Zgłoszenie błędu odbywa się poprzez rzucenie wyjątku. W Javie służy do tego słowo kluczowe
throw
:throw new IllegalArgumentException("ABC argument is invalid")
-
Nieobsłużony wyjątek powoduje przerwanie działania aplikacji.
-
Obsługa wyjątków odbywa się za pomocą mechanizmu przechwytywania wyjątków. W Javie służy do tego konstrukcja
try...catch
:try { // kod który może rzucić wyjątek } catch(IllegalArgumentException ex) { // kod obsługi wyjątku }
Wyjątek może być rzucony na dowolnym poziomie w kodzie, który otoczony jest blokiem
try
. Tzn. w kodzie tym może być wiele zagnieżdżonych wywołań funkcji, a i tak bloktry
przechwyci taki wyjątek, pod warunkiem, że nie zostanie on obsłużony na niższym poziomie. -
Wyjątki w Javie dzielą się na checked i unchecked. W pierwszym przypadku konieczna jest ich deklaracja (kompilator nie pozwoli zostawić rzuconego wyjątku bez jego obsługi), w drugim - wyjątki mogą być rzucane bez konieczności ich definiowania lub łapania (ale niezłapanie wyjątku wiąże się z przerwaniem wątku lub programu). Aby stworzyć wyjątek typu checked, wystarczy podziedziczyć po klasie
Exception
. Wyjątki unchecked dziedziczą z kolei poRuntimeException
. -
Wzorce projektowe są koncepcją występującą w programowaniu obiektowym polegającą na tym, że określona klasa problemów może być rozwiązana w schematyczny sposób. Rozwiązanie problemu jednak nie może być (najczęściej) zawarte w jednej klasie, dlatego wzorzec stanowi swego rodzaju szkielet rozwiązania, który określa jakie klasy i interfejsy muszą być wykorzystane, aby poprawnie rozwiązać dany problem.
-
Przykładem wzorca jest obserwator (observer) - rozwiązuje on problem zmian wewnętrznego stanu obiektu bez konieczności uzależniania klasy od wielu innych klas, które mają na te zmiany reagować.
-
Innym wzorcem jest metoda szablonowa (template method) - jest to sposób na wykorzystanie mechanizmu dziedziczenia i metod abstrakcyjnych do jeszcze większej redukcji powtarzającego się kodu.
-
Rekordy to proste struktury opisujące niezmienne (immutable) dane. Korzystamy z nich, by uniknąć żmudnego tworzenia getterów, setterów, konstruktorów, equals, hashCode i toString - wszystkie te elementy są automatycznie generowane!
public record Color(int red, int blue, int green) {}
Ten kod pozwala tworzyć obiekty kolorów i odwoływać się do nich tak podobnie w przypadku zwykłych klas:
Color color = new Color(255, 20, 10); int blue = color.blue(); System.out.println(color); // wypisze "Color[red=255, blue=20, green=10]"