Wzorce projektowe #2
Transkrypt
Wzorce projektowe #2
Wzorce projektowe #2 Ocena z zajęć Kod opracowanych przez studenta podlega ocenie. Termin oddania: 14 dni od terminu zajęć, do godziny 24:00. Całość powinna zostać umieszczona w SVN każdego studenta: https://ophelia.cs.put.poznan.pl/svn/io2009/infXXXXX/lab11/DP2/ Wzorce konstrukcyjne 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. 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 – klient korzystający z drzewiastej struktury danych ● Client ● Component – interfejs węzła (Composite) ● LeafX ● Composite zawierający wszystkie dostępne metody dla liści (Leaf) i – implementacje elementów końcowych – 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); }; 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); node1.operation(); Zadanie 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 Proxy Wiele obiektów jest kosztowna w utworzeniu, np. połączenie do bazy danych, ściągnięcie danych z sieci komputerowej. Ponadto spora część z tych danych może nawet nie być potrzebna do pracy, np. użytkownik może z książki zawierającej 200 stron przeczytać tylko wstęp zawierający 2 strony i zapoznać się z jednym obrazkiem (pomijając resztę). W celu zwiększenia efektywności często stosuje się różne rozwiązania. Dzięki nim można na przykład sprawić, by elementy często wykorzystywane były pobierane tylko raz i mogły być wielokrotnie używane. Przykładem takiego rozwiązania jest wzorzec Proxy, który pozwala na stworzenie dwóch obiektów wyglądających identycznie (implementują one ten sam interfejs), ale realizujących inne zadania. Pierwszy reprezentuje obiekt rzeczywisty, np. obrazek. Drugi, zwany obiektem proxy, będzie udawał obiekt docelowy i w momencie potrzeby wykonania operacji na nim – udostępni go. Oznacza to, że programista pracując na dowolnym z tych obiektów nie będzie widział żadnych różnic oraz dopóki obiektu rzeczywisty nie jest potrzebny do pracy, nie będzie on tworzony/pobierany/itd. Inne nazwy angielskie ● Surrogate Zastosowanie Wyróżniamy następujące rodzaje proxy: ● remote proxy – lokalna reprezentacja obiektu występującego w innej przestrzeni adresowej ● virtual proxy – tworzenie złożonych struktur na żądanie ● protection proxy – kontrola dostępu ● smart reference – warstwa pośrednicząca zapewniająca trwałość obiektu (persistent) lub inne czynności, np. zliczająca liczbę wywołań Model Diagram Uczestnicy – interfejs zawierający wspierane metody ● Subject ● RealSubject ● Proxy ● Client – implementacja obiektu docelowego – obiekt reprezentujący element pośredniczący odpowiedzialny za utworzenie i zarządzanie dostępem do RealSubject – klient Implementacja public abstract class Subject { public abstract void show(); } public class RealSubject extends Subject { public RealSubject(...) { ... } } public void show() { ... } public class Proxy extends Subject { private RealSubject rs = null; public Proxy(...) { ... } public void show() { if (rs == null) { rs = new RealSubject(...); } rs.show(); } } Proxy p = new Proxy(...); p.show(); Zadanie Wykorzystując wzorzec Proxy napisać program udostępniający dostęp do obrazka. Interfejs ma wspierać metody draw, load i save (mają one tylko wyświetlać komunikat informujący o wykonywanym poleceniu, np. nazwa metody). Nie przekazywać ścieżki do plik. Wzorce czynnościowe 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 – interfejs metod wpieranych przez każdy ze stanów ● State ● ConcreteStateX ● Context ● Client – różne implementacje metod w zależności od rodzaju stanu – klasa zarządzająca, która odpowiada za przekazywanie poleceń do aktualnego stanu – 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ązana zależy od ograniczeń z jakim ma się styczność, np. maksymalny czas oczekiwania na odpowiedź, częstość zmian, wykorzystanie wszystkich/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); } Zadanie Zrobić szkielet aplikacji do obsługi drukarki przyjmującej stany zgodnie z poniższym diagramem. Wpierać polecenie 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 uruchomi stop(). Mediator W programach często wykorzystuje się komponenty, które w pewien sposób oddziałują na siebie. Na przykład w ustawieniach edytora OpenOffice jeśli wyłączymy wykorzystanie Antyaliasingu (Screen font antyaliasing) opcje zależne staną się nieaktywne. Prawidłowe oprogramowanie takich zależności wymaga dużego nakładu pracy. Stanowi też duże wyzwanie od strony projektowej, by umożliwić łatwy rozwój i utrzymanie kodu. Wzorzec Mediator jest propozycją realizacji komponentów wymieniających informacje między sobą. Zakłada on istnienie zarządcy, który będzie odpowiedzialny za komunikację pomiędzy komponentami. Będzie on przyjmował komunikaty od komponentów, analizował je i przekazywał dalej. Nazwy polskie ● Mediator Zastosowanie ● komunikacja pomiędzy komponentami ● ponowne wykorzystanie obiektów ● ograniczenie elementów biorących udział w komunikacji Model Diagram Uczestnicy Wzorzec ten składa się z klasy, która pełni rolę zarządcy oraz komponentów (elementów zarządzanych). Każdy z elementów jest tworzony przez zarządcę i posiada możliwość komunikacji tylko z zarządcą. Komunikacja pomiędzy elementami jest niedozwolona. – interfejs zawierający metody dla zarządcy ● Mediator ● ConcreteMediator ● Colleague ● ConcreteColleague – zarządca, odpowiedzialny jest za stworzenie komponentów i koordynację wymiany danych pomiędzy nimi – interfejs komponentów – implementacja komponentu Implementacja public abstract class Mediator { public abstract void show(); public abstract void changed(Colleague colleague); } public class ConcreteMediator extends Mediator { @Override public void changed(Colleague colleague) { ... } } @Override public void show() { ... Colleague cc = new ConcreteColleague(this); cc.operation(); } public abstract class Colleague { public abstract void changed(); public abstract void operation(); } public class ConcreteColleague extends Colleague { private Mediator mediator; public ConcreteColleague(Mediator mediator) { this.mediator = mediator; } @Override public void changed() { ... mediator.changed(this); } @Override public void operation() { } ... changed(); } Mediator mediator = new ConcreteMediator(); mediator.show(); Zadanie Wykorzystując wzorzec Mediator zaimplementować logikę stojącą za widgetem odpowiedzialnym za konfigurację programu. Stworzyć 2 klasy. Pierwsza odpowiada za włączenie/wyłącznie konfiguracji (symulacja checkboxa), druga za ustawianie konkretnych pól (wystarczy 1 polecenie operation()). Jeżeli użytkownik wywoła polecenie wyłączenie konfiguracji, wszelkie zmiany w drugiej klasie powinny generować komunikat błędu. Command Programy użytkowe pozwalają na wywołanie tej samej operacji na wiele sposobów. Na przykład nowy dokument można utworzyć wybierając opcję w menu głównym aplikacji, z menu podręcznego albo paska zadań. Czynności tego typu jest bardzo wiele i umieszczanie tego samego kodu w wielu miejscach jest mało efektywne. Aby ułatwić zarządzanie takimi poleceniami oraz usprawnić ich utrzymanie warto pogrupować je i umieścić w jednym obiekcie. Dzięki temu będzie można go przekazywać w każde miejsce odpowiedzialne za uruchomienie danego polecenia. Wzorzec Command pozwala w efektywny sposób zrealizować to zadanie. Inne nazwy angielskie ● Action ● Transaction Nazwy polskie ● Polecenie Zastosowanie ● jeden mechanizm wykorzystywany w wielu miejscach ● metody callback ● wsparcie dla undo Model Diagram Uczestnicy Wzorzec zakłada stworzenie interfejsu Command, który będzie implementowany przez każdą klasę odpowiedzialną za jakieś zadanie (ConcreteCommand). Jego głównym składnikiem jest bezparametrowa metoda execute(), która jest uruchamiana za każdym razem, np. gdy użytkownik wybierze polecenie w menu. W związku z brakiem przekazywania jakichkolwiek parametrów, klasa implementująca musi sama wiedzieć jak ma zareagować. – interfejs z metodą execute() ● Command ● ConcreteCommand ● Invoker ● Receiver ● Client – implementacja polecenia, np. utworzenie pliku – wywołuje polecenie execute z ConcretCommand, np. polecenie w menu - obiekt na którym ConcreteCommand wykonuje operację, referencja do niego jest przekazywana zazwyczaj przy tworzeniu ConcreteCommand – tworzy instancje ConcretCommand i przekazuje ją do Invokera (np. do menu) Implementacja public abstract class Command { public abstract void execute(); } public class ConcreteCommand extends Command { private Receiver receiver; public ConcreteCommand(Receiver receiver) { this.receiver = receiver; } @Override public void execute() { ... receiver.doSth(); } } public class Invoker { public void setCommand(...) { ... } } public class Receiver { public void doSth() { ... } } Receiver receiver = new Receiver(); Command cmd = new ConcreteCommand(receiver); Invoker invoker = new Invoker(); invoker.setCommand(CommandType.CC, cmd); Zadanie Wywołać polecenie create() odpowiedzialne za utworzenie pliku (czyt. wyświetlenie komunikatu informującego o utworzeniu pliku) za pomocą wzorca Command.