Wzorce projektowe dr Jarosław Skaruz http://ii3.uph.edu.pl/~jareks jaroslaw@skaruz.com
O czym będzie? Wprowadzenie do wzorców projektowych Elementy wzorców Zalety stosowania wzorców Podział Strategia Singleton Dekorator
O czym będzie? Czego się spodziewałem? Jakiejś magii Wielkich nowości Co spotkałem? Znajome pomysły
Wzorce projektowe "Wzorzec opisuje problem, który pojawia się wielokrotnie w danym środowisku, i opisuje podstawę rozwiązania tego problemu, w taki sposób, że można użyć tego rozwiązania milion razy, bez konieczności ponownego wykonywania tych samych czynności" Christopher Alexander "A Pattern Language", 1977
Wzorce projektowe Czy zbudujemy most, opierając przęsło na kolejnych filarach połączonych łukiem, tak aby łuk usztywniał i naciągał przęsło stanowiąc jego podparcie na całej długości przęsła, czy też mocując płyty z obu stron za pomocą kilku lin stalowych o kolejno coraz krótszych długościach do pylonów umieszczonych pośrodku długości mostu?
Wzorce projektowe Czy zbudujemy most łukowy czy podwieszany?
Czym są wzorce? Wzorzec projektowy pozwala się uczyć na sukcesach innych zamiast nauki na własnych błędach. (Mark Johnson) Sprawdzone rozwiązania - Efektywność Najlepsze projekty - Jakość Dobre praktyki - Skalowalność
Czym są wzorce? W literaturze można spotkać się z różnymi definicjami: Wzorce projektowe stanowią powtarzalne rozwiązanie zagadnień projektowych, z którymi się wciąż spotykamy Wzorce projektowe stanowią zbiór reguł określających jak osiągnąć pewne cele w dziedzinie programowania Wzorzec adresowany jest do powtarzających się problemów, które pojawiają się w specyficznych momentach projektowania i stanowi dla nich rozwiązanie Wzorce projektowe w największym stopniu dotyczą problematyki proponowanego użycia powtarzających się motywów architektury programów, zaś szkielety aplikacji dotyczą szczegółów projektowych i implementacyjnych Każdy wzorzec jest złożoną z trzech elementów zasadą, wyrażającą relacje pomiędzy pewnym kontekstem, problemem i rozwiązaniem
Elementy wzorca Wzorzec projektowy pozwala się uczyć na sukcesach innych zamiast nauki na własnych błędach. (Mark Johnson) Nazwa wzorca - Odzwierciedla problem, rozwiązanie i konsekwencje danego wzorca Problem - Opisuje zagadnienie i kontekst wystąpienia wzorca, kiedy powinno się go stosować Rozwiązanie - Opisuje elementy tworzące projekt, ich relację, odpowiedzialności oraz ich współpracę Konsekwencje - Rezultaty zastosowania wzorca wady i zalety
Zalety stosowania wzorców Zapobiegają ponownemu wymyślaniu kodu zaprojektowanego już przez innych programistów Zmniejszają liczbę popełnionych błędów Zapewniają kod łatwo rozszerzalny Ułatwiają zrozumienie kodu Ułatwiają komunikacje w zespole
Podział wzorców Konstrukcyjne - wykorzystuje się je do pozyskania obiektów zamiast bezpośredniego tworzenia instancji klasy - programy zyskują na elastyczności, gdyż można decydować, jaki typ obiektu ma zostać utworzony w danym przypadku Strukturalne - pomagają łączyć obiekty w większe struktury Behawioralne - charakteryzują sposób, w jaki klasy lub obiekty oddziałują i dzielą odpowiedzialności - pomagają definiować przepływ danych w złożonym programie
Katalog wzorców Przeznaczenie Konstrukcyjne Strukturalne Behawioralne Zakres Klasa Metoda wytwórcza Adapter Interpreter Metoda szablonowa Obiekt Budowniczy Fabryka abstrakcyjna Prototyp Singleton Adapter Most Kompozyt Dekorator Fasada Pełnomocnik Pyłek Łańcuch zobowiązań Polecenie Iterator Mediator Pamiątka Obserwator Stan Strategie Odwiedzający
Katalog wzorców Jak NIE stosować wzorców projektowych Nie należy ich wykorzystywać byle jak Powinny być stosowane tylko wtedy, kiedy zapewniana przez niego elastyczność jest rzeczywiście potrzebna
Gra - Symulator Kaczki Zadanie programistyczne polega na implementacji gry, której akcja toczy się w stawie pełnym kaczek. Gra może wyświetlić wiele różnych gatunków kaczek, pływających oraz wydających odgłosy kwakania. Każdy podtyp kaczki jest odpowiedzialny za implementację swojego zachowania metody wyswietl() odpowiadającej za wygląd kaczki PROJEKT Metoda wyświetl jest metodą abstrakcyjną, ponieważ poszczególne podtypy kaczek różnią się od siebie
Zmiany projektowe W trakcie prac implementacyjnych dokonano ZMIANY wymagań. Obecnie kaczki muszą również posiadać możliwość latania. PROJEKT wszystko czego mi trzeba to dodać metodę lec() do klasy nadrzędnej wszystkie klasy podrzędne dziedziczą metodę leć()
Coś poszło nie tak, jak powinno Lokalna zmiana kodu programu spowodowała niepożądany globalny efekt uboczny (pojawienie się latających gumowych kaczek)! Jakie jest rozwiązanie? PROJEKT Poprzez umieszczenie metody lec() Jacek umożliwił latanie WSZYSTKIM kaczkom, włączając również te, które nie powinny latać. Gumowe kaczki nie kwaczą, stąd metoda kwacz() zmieniła zachowanie na piszcz.
A może by tak interfejs? Potrzebujemy rozwiązania, które pozwoli tylko wybranym gatunkom kaczek latać lub kwakać. ss Mógłbym usunąć metodę lec() z nadklasy i utworzyć interfejs Latajace posiadający metodę lec(). Wtedy wyłącznie kaczki latające będą implementowały ten interfejs. Co sądzisz o takim projekcie?
A może by tak interfejs? Weź to, co się zmienia i dokonaj odpowiedniej hermetyzacji, tak aby nie miało to żadnego wpływu na pozostałą część kodu. Rezultat? Mniej niezamierzonych ubocznych efektów będących konsekwencją wprowadzania nowych zmian w kodzie aplikacji oraz większą elastyczność całego systemu. Reguła projektowania Zidentyfikuj fragmenty aplikacji, które się zmieniają i oddziel je od tych, które pozostają stałe. Zadania: Aby wydzielić zachowania z nadklasy Kaczka, należy wyciągnąć metody lec() i kwacz() z wnętrza tej klasy Należy utworzyć zestaw nowych klas reprezentujących poszczególne zachowania Od tej chwili zachowania klasy Kaczka będą umieszczone w osobnej klasie, przechowującej implementację interfejsu reprezentującego dane zachowanie
Implementacja zachowań Kaczki Dzięki takiemu projektowi inne typy obiektów mogą wykorzystywać odpowiednie zachowania, ponieważ nie są one już dłużej ukryte w klasie nadrzędnej Kaczka! Można dodawać nowe zachowania bez konieczności modyfikacji jakichkolwiek istniejących klas opisujących dotychczasowe zachowania i bez modyfikacji tych klas opisujących Kaczki, które wykorzystują zachowania związane z lataniem!
Wzorzec Strategia hermetyzowane zachowania opisujące sposoby latania klient hermetyzowane zachowania opisujące sposoby kwakania
Test implementacji public abstract class Kaczka { private KwakanieInterfejs kwakanieinterfejs; private LataneiInterfejs latanieinterfejs; // więcej public void wykonajkwacz() { kwakanieinterfejs.kwacz(); public void wykonajlec() { latanieinterfejs.lec(); public void plywaj() { System.out.println( wszystkie kaczki pływają ); public abstract void wyswietl();
Test implementacji public interface LatanieInterfejs { public void lec(); public class LatamSzybko implements LatanieInterfejs{ public void lec() { System.out.println( szybko latam ); public interface KwakanieInterfejs { public void kwacz(); public class Kwacz implements KwakanieInterfejs { public void kwacz() { System.out.println( kwa kwa );
Test implementacji public class DzikaKaczka extends Kaczka { public DzikaKaczka() { kwakanieinterfejs = new Kwacz(); latanieinterfejs = new LatamSzybko(); public class SymulatorKaczki { public static void main(string[] args) { Kaczka dzika = new DzikaKaczka(); dzika.wykonajkwacz(); dzika.wykonajlec(); dzika.ustawlatanieinterfejs(new NieLatam()); dzika.wykonajlec();
Wzorzec Strategia Wzorzec Strategia definiuje rodzinę algorytmów, pakuje je jako osobne klasy i powoduje, że są one w pełni wymienne. Zastosowanie tego wzorca pozwala na to, aby zmiany w implementacji algorytmów przetwarzania były całkowicie niezależne od strony klienta, który z nich korzysta. Reguły projektowania obiektowego: Poddawaj hermetyzacji, to co się zmienia Przedkładaj kompozycję nad dziedziczenie Skoncentruj się na tworzeniu interfejsów, a nie implementacji
Klasyczna implementacja wzorca Singleton public class Singleton { Ta zmienna statyczna posłuży nam do przechowywania jedynej instancji obiektu klasy Singleton private static Singleton unikalnainstancja; private Singleton(){ Prywatny konstruktor, tylko obiekt klasy Singleton może z niego skorzystać public static Singleton pobierzinstance() { if(unikalnainstancja == null) unikalnainstancja = new Singleton(); return unikalnainstancja; Metoda pobierzinstancje() udostępnia sposób stworzenia instancji obiektu klasy Singleton i zwrócenia go jako wyniku działania tej metody
Wzorzec Singleton Wzorzec Singleton zapewnia, że dana klasa będzie miała tylko i wyłącznie jedna instancję obiektu i zapewnia globalny punkt dostępu do tej instancji.
Wzorzec Singleton Singleton jako wzorzec jest szczególnie przydatny do zastosowań wielowątkowości. UWAGA: poprzednia standardowa wersja nie będzie działała!!
Wzorzec Singleton a wielowątkowość public class Singleton { private static Singleton unikalnainstancja; private Singleton(){ public static synchronized Singleton pobierzinstance() { if(unikalnainstancja == null) unikalnainstancja = new singleton(); return unikalnainstancja; Na czym nadal polega problem?
Wzorzec Singleton a wielowątkowość public class Singleton { private static Singleton unikalnainstancja = new Singleton(); private Singleton(){ public static Singleton pobierzinstance() { return unikalnainstancja;
Projekt systemu dla Star Cafe Napoj to klasa abstrakcyjna, której podklasami są poszczególne rodzaje napojów oferowanych w kawiarni Metoda koszt jest abstrakcyjna; podklasy powinny posiadać jej implementację Każda podklasa posiada zaimplementowana metodę koszt(), która zwraca cenę danego napoju.
Projekt systemu dla Star Cafe c.d. Poszczególne metody koszt() obliczają cenę danego napoju przy uwzględnieniu odpowiedniej kawy oraz wszystkich zamówionych dodatków.
Refaktoryzacja projektu Na czym polega problem w bieżącym projekcie? Jakie zmiany należy dokonać w przypadku gdy: zmieni się cena mleka do kawy? firma wprowadzi nową polewę karmelową? Dziedziczeniem tu wygram! Zmienne typu boolean dla każdego produktu Te metody pobierają i zwracają wartości zmiennych logicznych Metoda koszt() w klasie nadrzędnej będzie wyliczała całkowity koszt wszystkich dodatków, a przesłaniające metody koszt() dołączają cenę napoju podstawowego Każda metoda koszt() musi policzyć cenę napoju podstawowego, a następnie dodać cenę dodatków wywołując w tym celu metodę koszt() z klasy nadrzędnej
Refaktoryzacja projektu c.d. Na czym polega problem w bieżącym projekcie? Jakie wymagania mogą zmieniać się tak, że będą miały wpływ na projekt? Zmiana cen poszczególnych dodatków będzie zmuszała do modyfikacji istniejącego kodu Wprowadzenie do oferty nowych dodatków będzie zmuszała do dodawania nowych metod oraz modyfikacji kodu metody koszt() Może się okazać, że w ofercie pojawią się nowe napoje. Dla niektórych napojów (np. mrożonej herbaty) pewne dodatki mogą być nieodpowiednie, aczkolwiek mimo wszystko podklasa Herbata powinna dziedziczyć takie metody jak np. zbitasmietana() A co w sytuacji kiedy klient zażyczy sobie podwójną porcję czekolady?
Reguła otwarte-zamknięte Reguła projektowania Klasy powinny być otwarte na rozbudowę, ale zamknięte na modyfikacje. Otwarte oznacza, że można dowolnie rozbudowywać klasy dowolnymi zachowaniami. Zamknięte oznacza, że kod musi być zamknięty na jakiekolwiek modyfikacje. Celem jest umożliwienie łatwej rozbudowy istniejącego kodu poprzez dodawanie im nowych zachowań, ale bez konieczności modyfikacji ich istniejącego kodu.
Zamówienie przy użyciu Dekoratorów 1. Klient zamawia kawę mocno paloną, więc tworzymy obiekt klasy MocnoPalona 2. Klient zażyczył sobie czekolady, więc tworzymy obiekt Czekolada i zawijamy go dookoła obiektu MocnoPalona 3. Klient życzy sobie również bitej śmietany, więc tworzymy dekorator BitaSmietana i zawijamy go dookoła obiektu Czekolada
Definicja wzorca Dekorator Wzorzec dekorator pozwala na dynamiczne przydzielanie danemu obiektowi nowych zachowań. Dekoratory dają elastyczność podobną do tej, jaką daje dziedziczenie, oferując jednak w zamian znacznie rozszerzoną funkcjonalność SkladnikPodstawowy to obiekt, któremu będziemy dodawali nowe zachowania Każdy składnik może być użyty samodzielnie lub w połączeniu z dekoratorem Każdy dekorator posiada swój składnik podstawowy referencję do obiektu dekorowanego DekoratorPodstawowy posiada referencję do obiektu dekorowanwego
Wzorzec Dekorator dla Star Cafe Abstrakcyjna klasa składników Cztery klasy składników podstawowych Dekoratory (dodatki), musza mieć zaimplementowana metodę pobierzopis() i koszt()
Kod aplikacji dla Star Cafe public abstract class Napoj { String opis = Napój nieznany ; public String pobierzopis() { return opis; public abstract double koszt(); public abstract class SkladnikDekorator extends Napoj{ public abstract String pobierzopis();
Kod aplikacji dla Star Cafe c.d. public class Espresso extends Napoj { public Espresso() { opis = Kawa Espresso ; public double koszt() { return 1.99; public class StarCafeSpecial extends Napoj{ public StarCafeSpecial() { opis = Kawa Star Cafe Special ; public double koszt() { return 1.99;
Kod aplikacji dla Star Cafe c.d. public class Czekolada extends SkladnikDekorator { Napoj napoj; public Czekolada(Napoj napoj) { this.napoj = napoj; public String pobierzopis() { return napoj.pobierzopis() +, Czekolada ; public double koszt() { return napoj.koszt() + 0,20;
Kod aplikacji dla Star Cafe c.d. public class StarCafe { public static void main(string[] args) { Napoj napoj = new Espresso(); System.out.println(napoj.pobierzOpis()+, + napoj.koszt()); Napoj napoj2 = new MocnoPalona(); napoj2 = new Czekolada(napoj2); napoj2 = new Czekolada(napoj2); napoj2 = new BitaSmietana(napoj2); System.out.println(napoj2.pobierzOpis()+, +napoj2.koszt()); Napoj napoj3 = new StarCafeSpecial(); napoj3 = new MleczkoSojowe(napoj3); napoj3 = new Czekolada(napoj3); napoj3 = new BitaSmietana(napoj3); System.out.println(napoj3.pobierzOpis()+, +napoj3.koszt());
Własny dekorator obsługi wejścia-wyjścia public class LowerCaseInputStream extends FilterInputstream { public LowerCaseInputStream (InputStream in) { super(in); public int read() throws IOException { int c = super.read(); return (c == -1? C : Character.toLowerCase((char)c)); public int read(byte[] b, int offset, int len) throws IOException { int result = super.read(b, offset, len); for(int i = offest; i < offset+result; i++) { b[i] = (byte)character.tolowercase((char)b[i]); return result;
Własny dekorator obsługi wejścia-wyjścia public class InputTest { public static void main(string[] args) throws IOException { int c; try { InputStream in = new LowerCaseInputStream( new BufferedInputStream( new FileInputstream( test.txt ))); while((c = in.read()) >= 0) { System.out.println((char)c); in.close(); catch(ioexception e) { e.printstacktrace();
Wzorzec Polecenie wymagania systemu Należy oprogramować siedem slotów tego urządzenia. Do każdego slotu można przypisać inne urządzenie i sterować jego pracą za pomocą odpowiednich przycisków Do każdego z siedmiu slotów przypisany jest osobny zestaw przycisków WŁĄCZ i WYŁĄCZ
Zestaw klas do obsługi urządzeń
Wzorzec Polecenie analiza przypadku public interface Polecenie() { public void wykonaj(); Wszystkie POLECENIA posiadają zaimplementowany ten sam interfejs, składający się tylko z jednej metody. public class PolecenieWlaczSwiatlo implements Polecenie { Swiatlo swiatlo ; public PolecenieWlaczSwiatlo(Swiatlo swiatlo) { this.swiatlo = swiatlo; public void wykonaj() { swiatlo.wlacz(); Metoda wykonaj() wywołuje metodę włącz() obiektu realizującego (czyli obiektu reprezentującego światło, którym sterujemy).
Zastosowanie POLECENIE public class MiniPilotTest { public static void main(string[] args) { MiniPilot pilot = new MiniPilot(); Swiatlo swiatlo = new Swiatlo(); PolecenieWlacz wlaczswiatlo = new PolecenieWlacz(swiatlo); pilot.ustawpolecenie(wlaczswiatlo); pilot.przyciskzostalnacisniety(); public class MiniPilot { Polecenie slot; public MiniPilot() { public void ustawpolecenie(polecenie polecenie) { this.slot = polecenie; public void przyciskzostalnacisniety() { slot.wykonaj();
Definicja Zastosowanie wzorca POLECENIE Polecenie Wzorzec Polecenie hermetyzuje żądania w postaci obiektów, co umożliwia parametryzowanie różnych obiektów zróżnicowanymi żądaniami (takimi jak np. żądania kolejkowania lub rejestracji) oraz obsługiwanie operacji, które można wycofać. Obiekt polecenie hermetyzuje żądanie poprzez wzajemne powiązanie zestawu operacji do wykonania z określonym obiektem, który będzie je realizował. Aby osiągnąć taki rezultat, zestaw operacji i obiekt realizujący są pakowane razem do obiektu, który udostępnia tylko jedną metodę, wykonaj(), która po wywołaniu powoduje, że dany zestaw operacji jest wykonywany np. rzez obiekt realizujący. Żaden inny obiekt nie wie jakie operacje będą wykonane ani kto jest obiektem realizującym takie operacje; obiekty zewnętrzne wiedzą jedynie, że jeżeli wywołają metodę wykonaj(), ich żądanie zostanie zrealizowane.
Definicja Zastosowanie wzorca Polecenie POLECENIE diagram klas Obiekt wywołujący przechowuje obiekt-polecenie i w określonym momencie prosi go o wykonanie żądania poprzez wywołanie jego metody wykonaj(). Odbiorca (obiekt realizujący) wie, w jaki sposób należy wykonać operacje niezbędne do realizacji określonego żądania. Rolę odbiorcy może spełniać dowolna klasa. Tworzy powiązanie pomiędzy operacjami, które powinny zostać wykonane, a odbiorcą tych operacji, czyli obiektem który będzie je realizował. Obiekt wywołujący wystawia żądanie poprzez wywołanie metody wykonaj() polecenia, a ten powoduje wykonanie żądania poprzez wywołanie odpowiednich metod obiektu realizującego.
Przypisywanie Zastosowanie POLECEŃ POLECENIE do slotów Zadanie Należy przypisać do każdego slotu SuperPilota odpowiednie POLECENIE. Dzięki temu SuperPilot zostanie naszym obiektem wywołującym. Po naciśnięciu przycisku pilota zostanie wywołana metoda wykonaj() obiektu przypisanego do slotu odpowiadającego temu przyciskowi, co w efekcie spowoduje wykonanie odpowiednich operacji przez obiekt realizujący.
Implementacja SuperPilota public void wcisnijwlacz(int slot) { poleceniewlacz[slot].wykonaj(); public void wcisnijwylacz(int slot) { poleceniewylacz[slot].wykonaj(); public class SuperPilot { Polecenie[] poleceniewlacz; Polecenie[] poleceniewylacz; public SuperPilot() { poleceniewlacz = new Polecenie[7]; poleceniewylacz = new Polecenie[7]; public void ustawpolecenie(int slot, Polecenie wlacz, Polecenie wylacz) { poleceniewlacz[slot] = wlacz; poleceniewylacz[slot] = wylacz;
Implementacja POLECEŃ public class PolecenieWylaczSwiatlo implements Polecenie { Swiatlo swiatlo ; public PolecenieWlaczSwiatlo(Swiatlo swiatlo) { this.swiatlo = swiatlo; public void wykonaj() { swiatlo.wylacz(); public class PolecenieWiezaStereoWlaczCD implements Polecenie{ WiezaStereo wiezastereo; public PolecenieWiezaStereoWlaczCD (WiezaStereo wiezastereo){ this.wiezastereo = wiezastereo; public void wykonaj() { wiezastereo.wlacz(); wiezastereo.ustawcd(); wiezastereo.ustawglosnosc(11);
Implementacja Wycofaj public class PolecenieWlaczSwiatlo implements Polecenie { Swiatlo swiatlo ; public PolecenieWlaczSwiatlo(Swiatlo swiatlo) { this.swiatlo = swiatlo; public void wykonaj() { swiatlo.wlacz(); public void wycofaj() { swiatlo.wylacz(); public class PolecenieWylaczSwiatlo implements Polecenie { Swiatlo swiatlo ; public PolecenieWylaczSwiatlo(Swiatlo swiatlo) { this.swiatlo = swiatlo; public void wykonaj() { swiatlo.wylacz(); public void wycofaj() { swiatlo.wlacz();
Implementacja Wycofaj c.d. public class SuperPilotZWycofywaniem { Polecenie[] poleceniewlacz; Polecenie[] poleceniewylacz; Polecenie poleceniewycofaj; public SuperPilotZWycofywaniem () { poleceniewlacz = new Polecenie[7]; poleceniewylacz = new Polecenie[7]; public void ustawpolecenie(int slot, Polecenie wlacz, Polecenie wylacz) { poleceniewlacz[slot] = wlacz; poleceniewylacz[slot] = wylacz; public void wcisnijwlacz(int slot) { poleceniewlacz[slot].wykonaj(); poleceniewycofaj = poleceniewlacz[slot]; public void wcisnijwylacz(int slot) { poleceniewylacz[slot].wykonaj(); poleceniewycofaj = poleceniewylacz[slot]; public void wcisnijwycofaj() { poleceniewycofaj.wycofaj();
Tak, to już KONIEC Dziękuję za uwagę!