Wstęp Stworzenie programu łatwego w rozwijaniu i naprawie nie należy do łatwych zadań. Na różnych etapach prac można napotkać wiele niemiłych niespodzianek i przeciwności losu, głównie takich które sami sobie zgotowaliśmy (np. nieodpowiednia architektura. W tym trudnym zadaniu warto skorzystać z doświadczenia innych ludzi i rozwiązań przez nich wypracowanych. Jednym ze sposobów zapobiegania sytuacjom niepożądanych są wzorce projektowe. Dostarczają one półproduktów rozwiązań dla często występujących problemów, np. jak jednym poleceniem wykonać wiele operacji, jak sprawić by ta sama metoda wykonywała różne czynności (bez zmiany parametrów), czy nawet jak nie powtarzać tego samego kodu w wielu miejscach. Stosowanie wzorców projektowych pozwala na wykorzystywanie uniwersalnych rozwiązań, a przez to uczynienie kodu bardziej zrozumiałym i mniej zawodnym. Należy jednak pamiętać, że nie należy wzorców aplikować automatycznie, tylko zawsze dopasować do aktualnego kontekstu użycia. można podzielić na kilka grup, do najpopularniejszych zalicza się: Wzorce konstrukcyjne (ang. creational design patterns) zbiór wzorców usprawniających pozyskiwanie obiektów Wzorce strukturalne (ang. structural design patterns) pomagają grupować obiekty w większe struktury Wzorce czynnościowe (ang. behavioral design patterns) pozwalają organizować komunikację pomiędzy obiektami, w tym przepływ danych Uwaga: Pojęcie interfejsu we wzorcach projektowych przekłada się na klasę abstrakcyjną, klasę lub interfejs w języku programowania. Na tych zajęciach poznamy po jednym klasycznym reprezentancie powyższych grup wzorców. strona 1 / 9
Wzorzec konstrukcyjny: Fabryka W niektórych programach nie zawsze wiadomo z jakimi obiektami będzie odbywała się współpraca. Na przykład, w OpenOffice można pracować z plikami ODF Text Document (*.odt), ze zwykłymi plikami tekstowymi (*.txt) oraz z wieloma innymi typami. Mimo różnić w ich budowie praca z nimi wygląda identycznie, mianowicie użytkownik tworzy nowy plik, wprowadza treść, modyfikuje ją, a na koniec zapisuje na dysk. Efekt taki można uzyskać w programie na wiele sposobów. Najprostszym z nich jest użycie wzorca Factory Method. Zakłada on opracowanie interfejsu zawierającego wszystkie operacje jakie nasz program będzie mógł wykonywać (na dokumencie) oraz klasy go implementujące (dla konkretnych już typów). Dzięki takiemu podejściu programista może operować na dowolnym dokumencie w ten sam sposób, nie wiedząc nawet jakiego on jest typu. Inne nazwy angielskie Virtual Constructor Nazwy polskie Metoda fabrykująca Fabryka Zastosowanie Brak wiedzy jaki dokładnie obiekt chce się utworzyć Chęć oddelegowania tworzenia obiektów Model Diagram Uczestnicy Product interfejs zawierający metody, które każdy obiekt wykorzystywany w programie musi wspierać. strona 2 / 9
ConcreteProduct implementacja konkretnego produktu. Creator interfejs zawierający metody, jakie obiekt tworzący Produkty musi zawierać. ConcreteCreator implementacja konkretnej fabryki. We wzorcu istnieją dwa interfejsy. Pierwszy (Product) definiuje jakie metody ma wspierać obiekt z którym aplikacja będzie pracować, drugi (Creator) określa metody potrzebne do stworzenie nowego obiektu. By powstał konkretny dokument (ConcreteProduct) musi istnieć klasa która go stworzy fabryka (ConcreteCreator). Jedna fabryka można tworzyć wiele produktów. Implementacja public abstract class Product { public abstract void method(); public class ConcreteProduct extends Product { public void method() { public abstract class Creator { public abstract Product createproduct(); public class ConcreteCreator extends Creator { public Product createproduct() { switch () { case : return new ConcreteProduct(); case : return new ; return null; Zadania // tworze fabryki Creator c = new ConcreteCreator(); // fabryka produkuje Product p1 = c.createproduct(); Product p2 = c.createproduct(); // praca z produktami Wykorzystując wzorzec Factory napisać program który pozwala wyświetlić zawartość pliku na różne sposoby (w zależności od jego typu). Jeżeli wybrany plik będzie plikiem tekstowym (*.txt) należy wyświetlić jego zawartość, dla skryptów (*.sh) wyświetlić pierwszą linijkę pliku, a dla reszty formatów wyświetlić komunikat o braku wsparcia. strona 3 / 9
Wzorzec strukturalny: Composite W celu łatwiejszego zarządzania danymi można je pogrupować. Rozwiązanie to jest jeszcze efektywniejsze, jeżeli operacje wykonywane na nich są bardzo zbliżone, bądź identyczne. Dobrze znanym przykładem jest tutaj obsługa elementów graficznych w programie. Przykładowo rysunek może składać się z linii, koła, kwadratu i zagnieżdżonego w nim rysunku, w skład którego mogą wchodzić wymienione elementy. Przykładowa budowa obrazka przedstawiona jest poniżej: Aby w łatwy sposób pracować z przedstawionymi obiektami można uporządkować je w strukturę drzewiastą. Elementem grupującym będzie Rysunek (pełnić one będą rolą węzła w drzewie), w skład którego wchodzą linie i figury geometryczne (liście). Wzorzec Composite zakłada identyczną strukturę danych. Dodatkowo wyróżnia on jeden interfejs, który implementują węzły i liście. Nazwy polskie Kompozyt Zastosowanie grupowanie elementów o takiej samej funkcjonalności automatyczne wykonywanie poleceń bez względu z jakim typem danych mamy o czynienia Model Diagram Uczestnicy Client klient korzystający z drzewiastej struktury danych strona 4 / 9
Component interfejs zawierający wszystkie dostępne metody dla liści (Leaf) i węzła (Composite) LeafX implementacje elementów końcowych Composite implementacja elementu grupującego Metoda operation() jest abstrakcyjna, gdyż każdy z elementów wykonuje ją inaczej. Leaf wykonuje konkretne zadanie, natomiast Composite wywołuje polecenia na elementach wchodzących w jego skład. Istnieją różne poglądy na temat umieszczania listy wszystkich metod w klasie Component (np. add() i remove()), można o nich poczytać w książce Design Patterns. Implementacja public abstract class Component { public abstract void operation(); public void add(component element) { ; public void remove(component element) { ; public Component getchild(int id) { return null; ; public class Leaf extends Component { private String data; public Leaf() { @Override public void operation() { public class Composite extends Component { private ArrayList<Component> elements = new ArrayList<Component>(); public Composite() { @Override public void operation() { for (Component element : elements) { element.operation(); public void add(component element) { elements.add(element); strona 5 / 9
; public void remove(component element) { elements.remove(element); ; public Component getchild(int id) { return elements.get(id); ; Component node1 = new Composite("node1"); Component leaf1 = new Leaf("leaf1"); Component leaf2 = new Leaf("leaf2"); node1.add(leaf1); node1.add(leaf2); Zadanie node1.operation(); Używając wzorca Composite napisać program odpowiedzialny za przechowywanie grafiki. Wyjściem programu powinny być nazwy wykonywanych klas, np.: composite line square circle composite line circle strona 6 / 9
Wzorzec czynnościowy: State Praca z danymi i urządzeniami często zależy od stanu w jakim się one znajdują. Np. aby móc wydrukować dokument, drukarka powinna być włączona i podłączona do komputera, powinno być odrobinę atramentu/tonera, itd. Spełnienie wszystkich z wymienionych warunków oznacza, że drukarka jest w stanie gotowości do pracy. Rozwiązań programistycznych dla podanej sytuacji jest wiele, jednym z nich jest wzorzec State. Idea jego polega na stworzeniu obiektu, który wykonanie konkretnego polecenie deleguje dalej, do obiektu reprezentującego konkretny stan. Programista zawsze może wykonać polecenie druku, ale tylko gdy urządzenie jest w odpowiednim stanie zostanie ono pomyślnie wykonane. Inne nazwy angielskie Objects for States Nazwy polskie Stan Zastosowanie Różna zachowanie obiektu w zależności od stanu w którym się znajduje Model Diagram Uczestnicy State interfejs metod wpieranych przez każdy ze stanów ConcreteStateX różne implementacje metod w zależności od rodzaju stanu Context klasa zarządzająca, która odpowiada za przekazywanie poleceń do aktualnego stanu strona 7 / 9
Client klient Przedstawiony diagram nie pokazuje kilku ważnych zagadnień związanych z używaniem tego wzorca, a dokładnie: momentu tworzenia stanów, przechowywanie danych wspólnych dla wszystkich stanów oraz zmiany pomiędzy stanami. Tworzenie stanów można rozwiązać na dwa sposoby. Pierwszym z nich jest tworzenie ich wraz z tworzeniem instancji klasy Context. Drugi to tworzenie instancji stanów w przypadku gdy są one potrzebne do pracy. Wybór rozwiązania zależy od ograniczeń, z którymi ma się styczność, np. maksymalny czas oczekiwania na odpowiedź, częstość zmian, wykorzystanie wszystkich lub części stanów, itp. Przechowywanie wspólnych danych zazwyczaj odbywa się w klasie Context, która przekazywana jest jako parametr do stanów. Zmiany pomiędzy stanami następują automatycznie i są one wykonywane przez klasę zarządzającą, bądź przez stan w jakim się aktualnie przebywa. Implementacja public abstract class State { abstract void operation(context ctx); public class ConcreteState extends State { void operation(context ctx) { ctx.setstate(ctx.getworkstate()); public class Context { private State state; private State cs; public Context() { cs = new ConcreteState(); state = ps; void setstate(state state) { this.state = state; State getconcretestate() { return cs; public void operation() { state.operation(this); strona 8 / 9
Zadanie Zrobić szkielet aplikacji do obsługi drukarki przyjmującej stany zgodnie z poniższym diagramem. Wpierać polecenia: start, stop, off, abort. Pierwsze 3 polecenia powinny być dostępne w trybie czuwania, ostatnie tylko podczas drukowania. Proces drukowania rozpoczyna się z chwilą wywołania polecenia start() i trwa aż programista wywoła stop(). strona 9 / 9