WZORCE PROJEKTOWE Języki i Techniki Programowania 7.XI.2003 Joanna Zięba
CZYM SĄ WZORCE PROJEKTOWE? „Wzorzec opisuje problem powtarzający się w środowisku naszego systemu i opisuje jego rozwiązanie w taki sposób, że może ono być wykorzystane wielokrotnie i na różne sposoby” (Huston Design Patterns - home.earthlink.net/~huston2/dp/patterns.ht ml)
ANALIZA WZORCÓW Należy wiedzieć, gdzie wzorców szukać (literatura, zasoby internetowe itp.), umieć je rozpoznawać w programach innych i wybierać najlepsze do swoich celów. Przy wyborze warto zwrócić uwagę na następujące aspekty zagadnienia: - kontekst (co, gdzie, kiedy...) - warunki wstępne - konsekwencje użycia danego rozwiązania - możliwe alternatywy (inne wzorce lub rezygnacja z ich stosowania na rzecz własnych rozwiązań)
KLASYFIKACJA WZORCÓW (konstrukcyjne, strukturalne, behawioralne)
WZORCE KONSTRUKCYJNE (creational patterns) Opisują jak obiekt może zostać stworzony. Powinny wyodrębniać szczegóły kreacji obiektów tak aby kod był niezależny od ich typów i nie musiał być zmieniany w miarę pojawiania się nowych rodzajów obiektów. Przykłady: Singleton, Fabryka, Fabryka Abstrakcyjna, Prototyp, Budowniczy
SINGLETON (Singleton) Gwarantuje, że dana klasa ma tylko jeden obiekt (instancję) i zapewnić 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
Prosty 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; } } Przykład z życia: „prezydent RP” – istnieje tylko co najwyżej jeden, a nazwa jednoznacznie identyfikuje sprawującą urząd osobę.
Inne wzorce, jak Fabryka Abstrakcyjna, Budowniczy i Prototyp mogą używać Singletona w swojej implementacji Singletonami są często obiekty typu Fasada (zwykle potrzeba tyko jednej) oraz Stan (powinien przyjmować jedną wartość)
FABRYKA (Simple Factory) 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 polimorficzna – struktura gdzie istnieje klasa - fabryka bazowa i jej różne podklasy – specyficzne fabryki.
FABRYKA – SCHEMAT
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.dra w"); } public void erase() { System.out.println("Circle.era se"); } 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((St ring)it.next())); it = shapes.iterator(); while(it.hasNext()) { Shape s = (Shape)it.next(); s.draw(); s.erase(); }
FABRYKA ABSTRAKCYJNA (Abstract Factory) Wzorca projektowego Abstract Factory można używać w celu otrzymania 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. Wzorzec ten jest fabryką, która zwraca jedną z wielu grup klas. Można nawet gdy używa się Simple Factory, decydować, którą klasę z tej grupy zwrócić. Można także wykorzystać w implementacji wzorzec Prototype, jeśli często będą tworzone obiekty o bardzo zbliżonych własnościach.
FABRYKA ABSTRAKCYJNA – SCHEMAT
PROTOTYP (Prototype) W tym wzorcu tworzenie obiektu polega na modyfikacji uprzednio utworzonej kopii pewnego obiektu - prototypu. Zastosowanie tego wzorca jest w szczególności uzasadnione, gdy tworzone obiekty zawiera dużą ilość czasochłonnie stworzonych danych, których tylko mała część jest modyfikowana względem prototypu. Przykład z życia: klonowanie, podział komórki
interface Xyz { Xyz cloan(); class Tom implements Xyz { public Xyz cloan() { return new Tom(); } public String toString() { return "ttt"; } } class Dick implements Xyz { public Xyz cloan() { return new Dick(); } public String toString() { return "ddd"; } } class Factory { private java.util.Map prototypes; public Factory(){ prototypes = new java.util.HashMap(); prototypes.put( "tom", new Tom() ); prototypes.put( "dick", new Dick() ); public Xyz makeObject( String s ) { return ((Xyz)prototypes.get(s)).cloan(); } } public static void main( String[] args ) { for (int i=0; i < args.length; i++) System.out.print( (new Factory()). makeObject( args[i] ) + " " );
BUDOWNICZY (Builder) Ten wzorzec jest podobny do wzorca Abstrakcyjnej Fabryki, gdyż służy on tworzeniu zbioru obiektów, ale tutaj tworzone obiekty są z sobą powiązane (w szczególności gdy tworzą Kompozyt). Obiekt złożony jest tworzony na podstawie danych wejściowych. Dane te są przetwarzane tak, że kolejne ich części tworzą komponenty, które z kolei mogą służyć wraz z innymi danymi utworzeniu obiektu złożonego.
BUDOWNICZY – SCHEMAT
WZORCE STRUKTURALNE (structural patterns) Pozwalają łączyć obiekty w większe struktury, mając zastosowanie np. w implementacji złożonego interfejsu użytkownika. Przykłady: Adapter, Most, Kompozyt, Dekorator, Fasada, Waga Piórkowa, Proxy
ADAPTER (Adapter) Wzorzec Adapter konwertuje interfejs jednej klasy na interfejs innej klasy. Używamy tego wzorca, jeśli chcemy, żeby dwie niezwiązane ze sobą klasy współpracowały ze sobą w jednym programie. Koncepcja wzorca Adaptera jest bardzo prosta: piszemy klasę, która posiada wymagany interfejs, a następnie zapewniamy jej komunikację z klasą, która ma inny interfejs. Istnieją dwa sposoby realizacji: poprzez dziedziczenie i poprzez kompozycję.
ADAPTER – PRZYKŁAD IMPLEMENTACJI package adapter; import junit.framework.*; class Target { public void request() {} } class Adaptee { public void specificRequest() { System.out.println("Adaptee: SpecificRequest"); } class Adapter extends Target { private Adaptee adaptee; public Adapter(Adaptee a) { adaptee = a; } public void request() { adaptee.specificRequest(); } public class SimpleAdapter extends TestCase { Adaptee a = new Adaptee(); Target t = new Adapter(a); public void test() { t.request(); } public static void main(String args[]) { junit.textui.TestRunner.run(Si mpleAdapter.class); }
KOMPOZYT (Composite) Wzorzec kompozytu pozwala na jednolite traktowanie komponentów i obiektów z nich złożonych poprzez specyfikację ich wspólnego interfejsu. Przykład z życia: 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 – SCHEMAT
Przykład: package composite; import java.util.*; import junit.framework.*; interface Component { void operation(); } class Leaf implements Component { private String name; public Leaf(String name) { this.name = name; } public String toString() { return name; } public void operation() { System.out.println(this); } class Node extends ArrayList implements Component { private String name; public Node(String name) { this.name = name; } public String toString() { return name; } public void operation() { System.out.println(this); for(Iterator it = iterator(); it.hasNext(); ) ((Component)it.next()).operation( ); } public class CompositeStructure extends TestCase { public void test() { Node root = new Node("root"); root.add(new Leaf("Leaf1")); Node c2 = new Node("Node1"); c2.add(new Leaf("Leaf2")); c2.add(new Leaf("Leaf3")); root.add(c2); c2 = new Node("Node2"); c2.add(new Leaf("Leaf4")); c2.add(new Leaf("Leaf5")); root.add(c2); root.operation(); } public static void main(String args[]) { junit.textui.TestRunner.run(Composi teStructure.class); }
DEKORATOR (Decorator) Wzorzec ten pozwala na dekorowanie zachowania klasy, czyli zmianę jej funkcjonalności bez potrzeby dziedziczenia, które mogłoby stworzyć zbyt wiele mało elastycznych klas. Najprostsza sytuacja:
FASADA (Facade) Często program podczas tworzenia ewoluuje i rośnie stopień jego komplikacji. Możemy zauważyć że oprócz korzyści wzorce mają też ujemną cechę: czasami generują one bardzo wiele dodatkowych klas, przez co trudniej jest zrozumieć działanie programu. Poza tym programy często składają się z szeregu podsystemów, z których każdy posiada swój własny skomplikowany interfejs. Dlatego też warto wprowadzić Fasadę – ujednolicony interfejs do szeregu interfejsów poszczególnych podsystemów.
FASADA - SCHEMAT
WAGA PIÓRKOWA (Flyweight) Waga Piórkowa ogranicza ilość tworzonych instancji obiektów, przez przeniesienie części danych z stanu obiektu do parametrów metod co umożliwia ich współdzielenie. Takie rozwiązanie wpływa korzystnie na szybkość wykonywania się programu – niekiedy niekontrolowane powstawanie zbyt dużej ilości obiektów spowalnia jego pracę.
Przykład: zamknięcie wielu obiektów w jednym (przetwarzanie ich osobno skrajnie nieefektywne z powodu dużej ich ilości) class DataPoint { private static int count = 0; private int id = count++; private int i; private float f; public int getI() { return i; } public void setI(int i) { this.i = i; } public float getF() { return f; } public void setF(float f) { this.f = f; } public String toString() { return "id: " + id + ", i = " + i + ", f = " + f; } public class ManyObjects { static final int size = ; public static void main(String[] args) { DataPoint[] array = new DataPoint[size]; for(int i = 0; i < array.length; i++) array[i] = new DataPoint(); for(int i = 0; i < array.length; i++) { DataPoint dp = array[i]; dp.setI(dp.getI() + 1); dp.setF(47.0f); } System.out.println(array[size -1]); }
Rozwiązanie: zamknięcie wszystkich DataPoint w jednym ExternalizedData class ExternalizedData { static final int size = ; static int[] id = new int[size]; static int[] i = new int[size]; static float[] f = new float[size]; static { for(int i = 0; i < size; i++) id[i] = i; } class FlyPoint { private FlyPoint() {} public static int getI(int obnum) { return ExternalizedData.i[obnum]; } public static void setI(int obnum, int i) { ExternalizedData.i[obnum] = i; } public static float getF(int obnum) { return ExternalizedData.f[obnum]; } public static void setF(int obnum, float f) { ExternalizedData.f[obnum] = f; } public static String str(int obnum) { return "id: " + ExternalizedData.id[obnum] + ", i = " + ExternalizedData.i[obnum] + ", f = " + ExternalizedData.f[obnum]; } public class FlyWeightObjects { public static void main(String[] args) { for(int i = 0; i < ExternalizedData.size; i++) { FlyPoint.setI(i, FlyPoint.getI(i) + 1); FlyPoint.setF(i, 47.0f); } System.out.println( FlyPoint.str(ExternalizedData.size -1)); }
WZORCE OPERACYJNE (behavioral patterns) pomagają zdefiniować komunikację pomiędzy obiektami oraz kontrolować przepływ danych w złożonym programie. Przykłady: Iterator, Łańcuch Odpowiedzialności, Interpreter, Stan, Mediator, Obserwator, Memento, Strategia
STAN (State) Wzorzec Stan pozwala obiektowi zmienić zachowanie gdy zmieni się jego stan wewnętrzny.
Przykład (bajkowy :-)) //: state:KissingPrincess.java package state; import junit.framework.*; class Creature { private boolean isFrog = true; public void greet() { if(isFrog) System.out.println("Ribbet!"); else System.out.println("Darling!"); } public void kiss() { isFrog = false; } } public class KissingPrincess extends TestCase { Creature creature = new Creature(); public void test() { creature.greet(); creature.kiss(); creature.greet(); } public static void main(String args[]) { junit.textui.TestRunner.run(Ki ssingPrincess.class); }
OBSERWATOR (Observer) Wzorzec ma na celu zdefiniowanie zależności miedzy obiektami typu jeden-do-wielu tak, aby przy zmianie stanu jednego pozostałe były o tym powiadamiane i też zmieniały swój stan.
ITERATOR (Iterator) Wzorzec Iteratora jest jednym z prostszych i najczęściej wykorzystywanych. Pozwala przemieszczać się poprzez listę lub dowolną kolejkę danych, z wykorzystaniem standardowego interfejsu, bez potrzeby znajomości wewnętrznej reprezentacji danych. Można też zdefiniować specjalne iteratory, które dokonują specjalnego przetwarzania i zwracają tylko niektóre elementy kolekcji danych.
Przykład użycia: public class TypedIterator implements Iterator { private Iterator imp; private Class type; public TypedIterator(Iterator it, Class type) { imp = it; this.type = type; } public boolean hasNext() { return imp.hasNext(); } public void remove() { imp.remove(); } public Object next() { Object obj = imp.next(); if(!type.isInstance(obj)) throw new ClassCastException( "TypedIterator for type " + type + " encountered type: " + obj.getClass()); return obj; }
Schemat innego zastosowania (C++) class ListIterator { public: ListIterator(const List& alist); virtual ~ListIterator() {} int AtEnd() const; void Restart(); int getPosition()const {return position;} protected: virtual ListElement* get(); virtual ListIterator& advance(); ListIterator& operator=(const ListIterator& other); ListIterator& operator=(const List& alist); private: ListElement* start; ListElement* cursor; int position; }; class ObjectListIterator:public ListIterator { public: ObjectListIterator(const ObjectList& alist); virtual ~ObjectListIterator() {} virtual ObjectListIterator& operator++(); //advances a cursor virtual ObjectListIterator& operator++(int); //same Object* Get(); //gets a pointer to an object void Set(Object*newobj=0); //sets a pointer to an object ObjectListIterator&operator=(const ObjectList& alist); };
WZORCE - KORZYŚCI + Sukces jest ważniejszy niż tworzenie wszędzie czegoś nowego + Dobrze napisane wzorce ułatwiają komunikację między autorami kodu + Pochodzą z praktycznego doświadczenia i rozwiązują konkretne problemy + Wzorce wcale nie służą eliminacji programistów!
ŹRÓDŁA I BIBLIOGRAFIA Gamma i inni (GoF): „Elements of Reusable Object-Oriented Software” James William Cooper „Java. Wzorce projektowe” (wyd. Helion) Bruce Eckel „Thinking in Patterns”, dostępne na Żródła internetowe – m.in. tutoriale na (Java Patterns 101 i 201)
MOTTO „Użycie wzorców projektowych pozwala nam uczyć się na sukcesach innych zamiast na własnych błędach”