Wzorce Projektowe Bartosz Baliś, Na podstawie Gamma et al. „Design Patterns, Elements ...” John Reekie „Design Patterns” Pascal Molli, „Design Patterns, Architectural Patterns” Bruce Eckel, „Thinking in Patterns” Źródła: 1. Russel Kay, QuickStudy: System Development Life Cycle, Computerworld, May 2002 http://www.computerworld.com/developmenttopics/development/story/0,10801,71151,00.html 2. http://www.answers.com 3. Ian Sommerville, Inżynieria Oprogramowania 4. Wikipedia
Wprowadzenie Wzorzec projektowy opisuje często powtarzający się problem oraz schemat rozwiązania tego problemu Wzorce są uogólnieniem faktycznych rozwiązań, które były stosowane przez projektantów Wzorce uchwytują specjalistyczne umiejętności ekspertów! Początkujący projektant/programista rozwiązałby problem bez wzorca, ale gorzej Wzorce projektowe dostarczają słownika, którym można opisywać projekt Gamma, Helm, Johnson, Vlissides (“Gang of Four”) – Design Patterns, Elements of Reusable Object-Oriented Software
Siedem warstw architektury* ORB OO architecture Global architecture Enterprise architecture Subsystem System architecture Application architecture Frameworks Macro-architecture Design patterns Micro-architecture Objects OO programming * Mowbray, Malveau, Corba Design Patterns
Siedem warstw architektury (c.d.) Architektura globalna: dotyczy koordynacji i komunikacji pomiędzy ogranizacjami Architektura korproracyjna (enterprise): dotyczy koordynacji i komunikacji w obrębie organizacji Architektura systemu: dotyczy koordynacji i komunikacji pomiędzy aplikacjami Architektura aplikacji (Subsystem): dotyczy dostarczania funkcjonalności Makro-architektura (Frameworks): dotyczy powtarzających się aplikacji Mikro-architektura: dotyczy wzorców projektowych Obiekty: dotyczy specyficznych idiomów języka programowania
Framework („Rama projektowa”) Częściowo kompletny (pod-)system, który jest podstawą do stworzenia wielu egzemplarzy systemów. Framework definiuje architekturę dla rodziny (pod-) systemów i dostarcza podstawowych elementów, aby takie systemy stworzyć. Framework definiuje miejsca, w których można wprowadzać adaptacje (implementacje specyficznej funkcjonalności) P. Molli
Dygresja: idiomy języków programowania Tak jak w językach naturalnych: specyficzne sposoby mówienia Np. PL „jestem głodny”, NIEM „Ich habe Hunger” PL „lepiej późno niż wcale”, ANG „better late than never” Przykład idiomów w C: Nie piszemy: for (i = 1; i <= n; i = i + 1) Ale: for (i = 0; i < n; i++) „Idiom” jako typ wzorca Niskopoziomowy wzorzec specyficzny dla danego języka programowania. Idiom opisuje jak zaimplementować określony aspekt komponentu (lub powiązanie między komponentami) przy użyciu cech danego języka.
Hierarchia wzorców (Bruce Eckel, TIP) Idiom: jak napisać kod w konkretnym języku programowania, aby zrobić konkretną rzecz (np. jak w C iterować po elementach tablicy) Specyficzny projekt (specific design): rozwiązanie, które opracowaliśmy dla danego konkretnego problemu. Może być sprytne, ale nie jest ogólne. Standardowy projekt (standard design): sposób rozwiązania pewnego rodzaju problemu (np. oddzielenie modelu od widoku) Wzorzed projektowy (design pattern): sposób rozwiązania całej klasy podobnych problemów. Zwykle uogólnienie standardowego projektu zastosowanego w kilku problemach.
Zasadnicze elementy wzorca projektowego Nazwa wzorca Wchodzi na stałe do słownika opisu archiektury Problem Opisuje, kiedy należy użyć dany wzorzec Rozwiązanie Abstrakcyjny opis projektu, ogólna struktura elementów rozwiązania (klas, obiektów) Konsekwencje Korzyści i koszty użycia wzorca
Przykład: model i widoki Model zawiera dane, które mogą być prezentowane na różne sposoby Model komunikuje się z widokami i jeśli dane się zmieniają, prezentacje również Uogólnienie problemu: rozdzielenie obiektów, tak że zmiany w jednym powodują zmiany w dowolnej ilości innych, bez konieczności znajomości dokładnej struktury jednych obiektów przez drugie Wzorzec Obserwator
Zestawienie wzorców projektowych Przeznaczenie Konstrukcyjne (Creational) Strukturalne (Structural) Czynnościowe (Behavioral) Zakres Klasa Metoda fabrykująca (Factory Method) Adapter Interpreter Szablon (Template) Obiekt Fabryka (Factory) Fabryka abstrakcji (Abstract Factory) Budowniczy (Builder) Prototyp (Prototype) Singleton Most (Bridge) Kompozyt (Composite) Dekorator (Decorator) Fasada (Facade) Pośrednik (Proxy) Łańcuch odpowiedzialności (Chain of Responsibility) Polecenie (Command) Iterator Mediator Memento Waga piórkowa (Flyweight) Obserwator (Observer) Stan (State) Strategia (Strategy) Wizytator (Visitor)
Typowe problemy, w których korzystnie jest użyć wzorców Tworzenie obiektu poprzez jawne określenie klasy Jawne określanie klasy gdy tworzymy obiekt wiąże nas z konkretną implementacją, zamiast z konkretnym interfejsem, co może utrudnić zmiany w przyszłości. Aby tego uniknąć, należy tworzyć obiekty w metodami pośrednimi. Wzorce: Fabryka abstrakcji, Metoda fabrykująca, Prototyp Zależność od określonych operacji Gdy bezpośrednio określamy konkretną operację, jesteśmy skazani na jeden sposób spełnienia żądania. Jeśli tego unikniemy, łatwiej będzie zmienić sposób obsługi żądania zarówno na poziomie kompilacji, jak i uruchomienia. Wzorce: Łańcuch odpowiedzialności, Polecenie
Typowe problemy (2) Zależność od platformy programowej i sprzętowej Zewnętrzne interfejsy do systemu lub aplikacji (API) różnią się na różnych platformach sprzętowych i programowych. Oprogramowanie, które jest uzależnione od danej platformy trudniej przenosi się na inne platformy, a nawet może być trudne do uaktualniania na macierzystej platformie. Zatem staramy się projektować tak, aby zminimializować zależności od platformy. Wzorce: Fabryka abstrakcji, Most Zależność od reprezentacji lub implementacji obiektu Klienci, którzy wiedzą jak obiekt jest reprezentowany, zapisywany czy implementowany i z tej wiedzy korzystają, muszą być modyfikowani, gdy obiekt jest zmieniany. Ukrywanie takich informacji zapobiega konieczności kaskadowych zmian. Wzorce: Fabryka abstrakcji, Most, Memento, Pośrednik
Typowe problemy (3) Zależność od algorytmu Algorytmy są często rozszerzane, optymalizowane i zmieniane. Obiekty, które są zależne od konkretnego algorytmu będą musiały być zmienione, gdy algorytm ulegnie zmianie. Dlatego należy projektować tak, aby wyeliminować zależność od konkretnego algorytmu. Wzorce: Budowniczy, Iterator, Strategia, Szablon metod, Wizytator Ścisłe połączenie (tight coupling) Ściśle połączone klasy są trudne do ponownego użycia osobno. Ścisłe połączenie prowadzi do systemów monolitycznych, gdzie jedna zmiana pociąga zmiany w wielu innych miejscach. Luźne połączenie (loose coupling) sprzyja ponownemu użyciu, przenośności i rozszerzalności. Przy pomocy wzorców projektowych można zmniejszać ścisłość połączenia w systemie. Wzorce: Fabryka abstrakcji, Most, Łańcuch odpowiedzialności, Polecenie, Fasada, Mediator, Obserwator
Typowe problemy (4) Rozszerzanie funkcjonalności poprzez tworzenie klas potomnych Rozszerzanie funkcjonalności poprzez klasy potomne niesie ze sobą szereg trudności, konieczność dokładnej znajomości klasy podstawowej. Np. przesłonięcie jednej metody może wymagać przesłonięcia innej. Alternatywą dla tworzenia klas potomnych jest kompozycja obiektów. Z drugiej strony używanie kompozycji utrudnia zrozumienie projektu. Wiele wzorców projektowych umożliwia rozszerzeanie funkcjonalności poprzez jednokrotne tworzenie klasy potomnej i kompozycję jej instancji z istniejącymi klasami. Wzorce: Most, Łańcuch odpowiedzialności, Kompozyt, Dekorator, Obserwator, Strategia Brak możliwości wygodnej modyfikacji klasy Czasem istnieje konieczność modyfikacji klasy, gdy nie da się tego zrobić w sposób wygodny (nie ma kodu źródłowego, trzeba byłoby modyfikować wiele klas potomnych, itp.) Wzorce projektowe pozwalają na modyfikacje klasy w takich sytuacjach. Wzorce: Adapter, Dekorator, Wizytator
Wzorzec projektowy vs. Framework Wzorce są bardziej niskopoziomowe niż frameworki Framework zwykle składa sie z wielu wzorców: Fabryka Strategia Kompozyt Obserwator
Wzorce konstrukcyjne
Singleton Np. „prezydent USA” – istnieje co najwyżej jeden Gwarantuje, że dana klasa ma tylko jeden obiekt (instancję) i zapewna globalny sposób dostępu do tego obiektu. Obiekt stworzony wg tego wzorca może zastąpić zmienną globalną. Podejście Klasę typu Singleton należy uczynić odpowiedzialną za tworzenie, inicjalizację, dostęp i ew. zmiany obiektów. Sam obiekt musi być jej składnikiem typu private static, a funkcja inicjalizacji i dostępu – public static Np. „prezydent USA” – istnieje co najwyżej jeden Jest też określany jako „idiom”
Singleton – przykład class Singleton { private Singleton s; private int i; private Singleton(int x) { i = x; } public static Singleton getReference() { if (s == null) s = new Singleton(2); return s; } public int getValue() { return i; } public void setValue(int x) { i = x; } }
Fabryka Wzorzec ten w zależności od dostarczonych danych, zwraca instancję jednej z możliwych klas. Najczęściej zwracane klasy wywodzą się z tej samej klasy podstawowej i mają takie same metody, ale każda z nich wykonuje swoje zadania w inny sposób i jest zoptymalizowana dla innego rodzaju danych. Rozwinięciem tego wzorca jest fabryka abstrakcyjna – struktura gdzie istnieje klasa - fabryka bazowa i jej różne podklasy – specyficzne fabryki.
Fabryka – schemat
Fabryka – przykład abstract class Shape { public abstract void draw(); public abstract void erase(); public static Shape factory(String type) { if(type.equals("Circle")) return new Circle(); if(type.equals("Square")) return new Square(); throw new RuntimeException( "Bad shape creation: " + type); } } class Circle extends Shape { Circle() {} // Package-access constructor public void draw() { System.out.println("Circle.draw"); public void erase() { System.out.println("Circle.erase"); public class ShapeFactory1 extends TestCase { String shlist[] = { "Circle", "Square", "Square", "Circle", "Circle", "Square" }; List shapes = new ArrayList(); public void test() { Iterator it = Arrays.asList(shlist).iterator(); while(it.hasNext()) shapes.add(Shape.factory((String)it.next())); it = shapes.iterator(); while(it.hasNext()) { Shape s = (Shape)it.next(); s.draw(); s.erase(); } }
Fabryka abstrakcyjna Cel Otrzymanie jednej z wielu związanych ze sobą klas obiektów, z których każdy może na żądanie zwrócić wiele innych obiektów
Fabryka abstrakcyjna – schemat
Fabryka (abstrakcyjna) – kiedy używać System ma być niezależny od tego jak jego produkty są tworzone, komponowane i reprezentowane System ma być skonfigurowany z jedną z wielu rodzin produktów Rodzina obiektów powiązanego produktu jest zaprojektowana do wspólnego używania i chcemy to wymusić Chcemy dostarczyć bibliotekę produktów, ale chcemy ujawnić tylko ich interfejsy, nie implementacje
Wzorce strukturalne
Adapter Konwertuje interfejs jednej klasy na interfejs innej klasy, którego spodziewa się klient Adapter umożliwia współpracę niekompatybilnych klas
Adapter – struktura P. Molli
Adapter – przykład P. Molli
Kompozyt Konstrukcja hierarchii całość-część Upraszcza interfejs klienta do liści/kompozytów Łatwiej dodawać nowe komponenty Wzorzec kompozytu pozwala na jednolite traktowanie komponentów i obiektów z nich złożnych poprzez specyfikację ich wspólnego interfejsu. Przykład: zapis działania matematycznego, składa się ono z liczb, symboli operatorów i nawiasów; także przepis kuchenny, jeśli za komponenty uznamy poszczególne składniki.
Kompozyt – struktura
Kompozyt – przykład P. Molli
Kompozyt – przykład (obiekty) P. Molli
Fasada Jednolity interfejs do zbioru interfejsów w systemie Ukrywa i oddziela podsystemy przed klientami Client Facade
Fasada – przykład SchematicEditor Graph Director Relation Port Entity Interfejs do silnika symulacji SchematicEditor Graph Director 2 0..* Relation Port Entity BufferedRelation AtomicEntity CompositeEntity Token Actor
Most Oddziela abstrakcję od implementacji, tak że mogą one zmieniać się niezależnie od siebie Zwiększa łatwość rozbudowy Ukrywa przed klientem szczegóły implementacji
Most – struktura
Most – przykład
Most – kiedy używać Gdy chcemy uniknąć trwałego połączenia abstrakcji i jej implementacji. Np. implementacja ma być wybrana lub zmieniona w trakcie wykonania Zarówno abstrakcje jak i implementacje powinny być rozszerzalne przez tworzenie klas potomnych. Most pozwala na łączenie różnych abstrakcji z implementacjami i ich niezależną rozbudową Zmiany w implementacji nie powinny mieć wpływu na klientów (nie powinno być konieczności ich rekompilacji) Gdy chcemy używać jednej implementacji w wielu obiektach jednocześnie (np. przez licznik referencji) i ma to być ukryte przed klientem Prosty przykład: wiele obiektów String może odnosić się do tego samego łańcucha w pamięci
Wzorce czynnościowe
Stan pozwala obiektowi zmienić zachowanie gdy zmieni się jego stan wewnętrzny
Stan – przykład
Stan – kiedy używać Zachowanie obiektu jest zależne od stanu, w jakim się znajduje i musi być ono zmieniane w trakcie wykonania Operacje zawierają wielokrotne instrukcje warunkowe, zależne od stanu obiektu (zwykle wartości jakiejś zmiennej typu wyliczeniowego). Często wiele operacji będzie zawierać tę samą strukturę instrukcji warunkowych. Stan pozwala umieścić każde odgałęzienie instrukcji warunkowej w osobnej klasie, co pozwala na traktowanie stanu jako osobnego obiektu, który może się zmieniać niezależnie od innych obiektów.
Strategia Context Strategy ConcreteStrategy1 ConcreteStrategy2 Pozwala na podmianę algorytmów Alternatywa dla tworzenia klas pochodnych Wybór implementacji w trakcie wykonania Zwiększa złożoność w trakcie wykonania Context Strategy ContextInterface() Operation() ConcreteStrategy1 ConcreteStrategy2 Operation() Operation()
Strategia – przykład P. Molli
Strategia – kiedy używać Wiele powiązanych klas różni się tylko zachowaniem. Strategia pozwala na możliwość konfiguracji klasy z jednym wybranym zachowaniem Potrzebujemy różnych odmian algorytmu Algorytm używa danych, o których klient powinien wiedzieć. Strategia pozwala uniknąć ujawniania złożonych specyficznych dla algorytmu struktur danych Klasa definiuje wiele zachowań, które pojawiają się w wielokrotnych instrukcjach warunkowych. Zamiast nich można każdą gałąź instrukcji warunkowej przenieść do osobnej klasy strategii
Dalsza lektura FAQ Bruce Eckel, Thinking in Patterns Podstawowe wzorce http://g.oswego.edu/dl/pd-FAQ/pd-FAQ.html Bruce Eckel, Thinking in Patterns http://www.mindview.net/Books/TIPatterns Podstawowe wzorce http://exciton.cs.oberlin.edu/javaresources/DesignPatterns/default.htm Patterns home page http://hillside.net/patterns