User Tools

Site Tools


Problem constructing authldap
arhiva:laboratoare:2013:lab11

Design patterns

  • Responsabil: Adriana Drăghici
  • Data publicării: 15.12.2013
  • Data ultimei modificări: 18.12.2013

Obiective

Scopul acestui laborator este familiarizarea cu folosirea unor pattern-uri des întâlnite în design-ul atât al aplicațiilor, cât și al API-urilor - Singleton, Observer, Command și Factory.

Introducere

În laboratorul Visitor Pattern au fost introduse design pattern-urile și aplicabilitatea Visitor-ului. Acesta este un pattern comportamental, și după cum ați observat oferă avantaje în anumite situații, în timp ce pentru altele nu este potrivit. Pattern-urile comportamentale modelează interacțiunile dintre clasele și componentele unei aplicații, fiind folosite în cazurile în care vrem sa facem un design mai clar și ușor de adaptat și extins. În afară de acest tip de pattern-uri, mai se folosesc și cele structural și creational, prezentate în clasificarea următoare:

  • Creational Patterns - mecanisme de creare a obiectelor
    • Singleton, Factory etc
  • Structural Patterns - definesc relații între entități
    • Decorator, Adapter, Facade, Composite, Proxy etc.
  • Behavioural Patterns - definesc comunicarea între entități
    • Visitor, Observer, Command, Mediator, Strategy etc.

Singleton Pattern

Pattern-ul Singleton este utilizat pentru a restrictiona numarul de instantieri ale unei clase la un singur obiect, deci reprezintă o metodă de a folosi o singură instanță a unui obiect în aplicație.

Utilizari

Pattern-ul Singleton este in general utilizat in urmatoarele cazuri:

  • subansamblu al altor pattern-uri:
    • impreună cu pattern-urile Abstract Factory, Builder, Prototype etc.
  • obiectele care reprezintă stări
  • în locul variabilelor globale. Singleton este preferat variabilelor globale deoarece, printre altele, nu poluează namespace-ul global cu variabile care nu sunt necesare.

Din punct de vedere al design-ului și testarii unei aplicații de multe ori se evită folosirea acestui pattern. De exemplu dacă avem nevoie sa menținem o referință către un obiect în mai multe clase, putem sa îl facem Singleton și să obținem acel obiect în fiecare componentă în care avem nevoie de el sau ca sa facem codul mai curat și cu un flow mai ușor de urmărit, să îl instanțiem doar într-un singur loc și să îl transmitem ca argument.

Încercați să nu folosiți în exces metode statice (să le utilizați mai mult pt funcții “utility”) și componente Singleton.

Implementare

Aplicarea pattern-ului Singleton constă în aplicarea unei metode ce permite crearea unei noi instanțe a clasei dacă aceasta nu exista deja. Daca instanța există deja, atunci întoarce o referință către acel obiect. Pentru a asigura o singură instanțiere a clasei, constructorul trebuie să fie private, iar instanța să fie oferită printr-o metodă statică, publică.

În cazul unei implementări Singleton, clasa respectivă va fi instanțiată lazy (lazy instantiation), utilizând memoria doar în momentul în care acest lucru este necesar deoarece instanța se creează atunci când se apelează getInstance(), acest lucru putând fi un avantaj în unele cazuri, față de clasele non-singleton, pentru care se face eager instantiation, deci se alocă memorie încă de la început, chiar dacă instanța nu va fi folosită (mai multe detalii și exemplu în acest articol)

Fig. 1: Diagrama de clase pentru Singleton

Respectând cerințele pentru un singleton enunțate mai sus, în Java, putem implementa o componentă de acest tip în mai multe feluri, inclusiv folosind enum-uri în loc de clase. Atunci când îl implementâm trebuie avut în vedere contextul în care îl folosim, astfel încât să alegem o soluție care să funcționeze corect în toate situațiile ce pot apărea în aplicație (unele implementări au probleme atunci când sunt accesate din mai multe thread-uri sau când trebuie serializate).

 public class Singleton {
   private Singleton instance = null;
 
   public static Singleton getInstance() {
      if(instance == null) 
          return new Singleton();
   }
   ...
 }
  • Instanța instance este private
  • Constructorul apelat este constructorul privat default
  • Instanța este inițial nulă
  • Instanța este creată la prima rulare a getInstance()

Codul din exemplul de mai sus funcționează corect în context single-threaded, însă în context multi-threading apar probleme legate de ce instanță ajung să obțină thread-urile. Aparent problema poate fi rezolvată folosind keyword-ul synchronized, însă nici acest lucru nu este suficient, soluția fiind de fapt folosirea keyword-ului static, ca în exemplul următor:

public class Singleton {
    private static Singleton instance = new Singleton();
 
    public static Singleton getInstance(){
        return instance;
    }
    ... 
}
  • Câmpul static nu este inițializat până când nu este referit prima oară
  • Toate thread-urile vor avea acces la rezulatele inițializării.

De ce Singleton și nu clase cu membri statici?

O clasă de tip Singleton poate fi extinsă și metodele ei suprascrise, însă într-o clasă cu metode statice acestea nu pot fi suprascrise (overriden) (o discuție pe aceasta temă puteți gasi aici, și o comparatie între static și dynamic binding aici.

Aplicabilitate

Acest pattern este utilizat des în situații în care avem obiecte care trebuie accesate din mai multe locuri ale aplicației:

  • obiecte de tip logger
  • obiecte care reprezintă resurse partajate (conexiuni, sockets etc.)
  • obiecte ce conțin con gurații pentru aplicație
  • pentru obiecte de tip Factory.

Exemple din API-ul Java:

Folosirea componentelor Singleton poate să prezinte dezavantaje în multe cazuri, de exemplu în test-driven development este considerat chiar un anti-pattern. Codul care folosește stări globale este mai dificil de testat pentru că implică o cuplare mai strânsă a claselor, iar în testare de multe ori se dorește izolarea unei componente și testarea ei individuală. Dacă o clasă testată folosește un obiect singleton, atunci trebuie testat și singleton-ul. Soluția este simularea mock-up a singleton-ului în teste. Încă o problemă a acestei cuplări mai strânse apare atunci când două teste depind unul de celălalt prin modificarea singleton-ului, deci trebuie impusă o anumită ordine a rulării testelor.

Factory

Patternurile de tip Factory sunt folosite pentru obiecte care generează instanțe de clase înrudite (implementează aceeași interfață, moștenesc aceeași clasă abstractă). Acestea sunt utilizate atunci când dorim să izolăm obiectul care are nevoie de o instanță de un anumit tip, de creearea efectivă acesteia. În plus clasa care va folosi instanța nici nu are nevoie să specifice exact clasa obiectului ce urmează a fi creat, deci nu trebuie să cunoască toate implementările acelui tip, ci doar ce caracteristici trebuie să aibă obiectul creat. Din acest motiv, Factory face parte din categoria Creational Patterns, deoarece oferă o soluție legată de creearea obiectelor.

Aplicabilitate:

  • în biblioteci/API-uri, utilizatorul este separat de implementarea efectivă a tipului (vedeți în java core api, java nio etc.)
  • crearea obiectelor este mai complexă (trebuie realizate mai multe etape etc.), atunci separăm logica necesară instanțierii subtipului de clasa care are nevoie de instanță.

Abstract Factory Pattern

Fig. 2: Diagrama de clase pentru Abstract Factory

Codul următor corespunde diagramei din figure 2. În acest caz folosim interfețe pentru factory și pentru tip, însă în alte situații putem să avem direct SpecializedFooFactory, fără a implementa interfața FooFactory.

public interface Foo {
    public void bar();
}
public interface FooFactory {
    public Foo createFoo();
}
public class SpecializedFoo implements Foo {
    ... 
}
public class SpecializedFooFactory implements FooFactory {
    public Foo createFoo() {
        return new SpecializedFoo();
    }
}

Factory Method Pattern

Folosind pattern-ul Factory Method se poate defini o interfață pentru crearea unui obiect, însă ce obiecte este instațiat este dat de către subclase.

Spre deosebire de Abstract Factory, Factory Method ascunde construcția unui obiect, nu a unei familii de obiecte “inrudite”, care extind un anumit tip. Clasele care implementează Abstract Factory conțin de obicei mai multe metode factory.

Fig. 3: Diagrama de clase pentru Factory Method

Exemplu

Situația cea mai întâlnită în care se potrivește acest pattern este aceea când trebuie instantiate multe clase care implementează o anumită interfață sau extind o altă clasă (eventual abstractă), ca în exemplul de mai jos. Clasa care folosește aceste subclase nu trebuie să “știe” tipul lor concret ci doar pe al părintelui. Implementarea de mai jos corespunde pattern-ului Abstract Factory pentru clasa PizzaFactory, și foloseste factory method pentru metoda createPizza.

PizzaLover.java
abstract class Pizza {
    public abstract double getPrice();
}
class HamAndMushroomPizza extends Pizza {
    public double getPrice() {
        return 8.5;
    }
}
class DeluxePizza extends Pizza {
    public double getPrice() {
        return 10.5;
    }
}
class HawaiianPizza extends Pizza {
    public double getPrice() {
        return 11.5;
    }
}
 
class PizzaFactory {
    public enum PizzaType {
        HamMushroom, Deluxe, Hawaiian
    }
    public static Pizza createPizza(PizzaType pizzaType) {
        switch (pizzaType) {
            case HamMushroom: return new HamAndMushroomPizza();
            case Deluxe:      return new DeluxePizza();
            case Hawaiian:    return new HawaiianPizza();
        }
        throw new IllegalArgumentException("The pizza type " + pizzaType + " is not recognized.");
    }
}
public class PizzaLover {
    public static void main (String args[]) {
        for (PizzaFactory.PizzaType pizzaType : PizzaFactory.PizzaType.values()) {
            System.out.println("Price of " + pizzaType + " is " + PizzaFactory.createPizza(pizzaType).getPrice());
        }
    }
}
Output:
Price of HamMushroom is 8.5
Price of Deluxe is 10.5
Price of Hawaiian is 11.5

Singleton Factory

De obicei avem nevoie ca o clasă factory să fie utilizată din mai multe componente ale aplicației. Ca să economisim memorie este suficient să avem o singură instanță a factory-ului și să o folosim pe aceasta. Folosind pattern-ul Singleton putem face clasa factory un singleton, și astfel din mai multe clase putem obține instanță acesteia.

Un exemplu ar fi Java Abstract Window Toolkit (AWT) ce oferă clasa abstractă java.awt.Toolkit care face legătura dintre componentele AWT și implementările native din toolkit. Clasa Toolkit are o metodă factory Toolkit.getDefaultToolkit() ce întoarce subclasa de Toolkit specifică platformei. Obiectul Toolkit este un Singleton deoarece AWT are nevoie de un singur obiect pentru a efectua legăturile și deoarece un astfel de obiect este destul de costisitor de creat. Metodele trebuie implementate în interiorul obiectului și nu pot fi declarate statice deoarece implementarea specifică nu este cunoscută de componentele independente de platformă.

Observer Pattern

Design Pattern-ul Observer definește o relație de dependență 1..* între obiecte astfel încât când un obiect își schimbă starea, toți dependenții lui sunt notificați și actualizați automat. Folosirea acestui pattern implică existența unui obiect cu rolul de subiect, care are asociată o listă de obiecte dependente, cu rolul de observatori, pe care le apelează automat de fiecare dată când se întâmplă o acțiune.

Acest pattern este de tip Behavioral (comportamental), deorece facilitează o organizare mai bună a comunicației dintre clase în funcție de rolurile/comportamentul acestora.

Observer se folosește în cazul în care mai multe clase(observatori) depind de comportamentul unei alte clase(subiect), în situații de tipul:

  • o clasă implementează/reprezintă logica, componenta de bază, iar alte clase doar folosesc rezultate ale acesteia (monitorizare).
  • o clasă efectuează acțiuni care apoi pot fi reprezentate în mai multe feluri de către alte clase (view-uri ca în figură de mai jos).

Practic în toate aceste situații clasele Observer observă modificările/acțiunile clasei Subject. Observarea se implementează prin notificări inițiate din metodele clasei Subject.

Structură

Pentru aplicarea acestui pattern, clasele aplicației trebuie să fie structurate după anumite roluri, și în funcție de acestea se stabilește comunicarea dintre ele. În exemplul din figure 4, avem două tipuri de componente, Subiect și Observator, iar Observator poate fi o interfață sau o clasă abstractă ce este extinsă cu diverse implementări, pentru fiecare tip de monitorizare asupra obiectelor Subiect.

  • observatorii folosesc datele subiectului
  • observatorii sunt notificați automat de schimbări ale subiectului
  • subiectul cunoaște toți observatorii
  • subiectul poate adăuga noi observatori

Fig. 4: Diagrama de clase pentru Observer Pattern

Subiect

  • nu trebuie să știe ce fac observatorii, trebuie doar să mențină referințe către obiecte de acest tip
  • nu știe ce fac observatorii cu datele
  • oferă o metodă de adăugare a unui Observator, eventual și o metodă prin care se pot deinregistra observatori
  • menține o listă de referințe cu observatori
  • când apar modificări (e.g. se schimbă starea sa, valorile unor variabile etc) notifică toți Observatorii

Observator

  • definește o interfață notificare despre schimbări în subiect
  • ca implementare:
    • toți observatorii pentru un anumit subiect trebuie să implementeze această interfață
    • metoda (metode) ce sunt invocate de către subiect pentru a notifica o schimbare. Ca argumente se poate primi chiar instanța subiectului sau obiecte speciale care reprezintă evenimentul ce a provocat schimbarea

View/ObservatorDerivat

  • implementeaza interfața Observator

Aceasta schema se poate extinde, în funcție de aplicație, observatorii pot ține referințe catre subiect sau putem adauga clase speciale pentru reprezentarea evenimentelor, notificarilor. Un alt exemplu il puteti gasi aici.

Implementare

Un exemplu de implementare este exercitiul 2 de la laboratorul 6 (Clase interne). Urmăriți soluția acestuia și diagrama de clase pentru acesta.

Tookit-urile GUI, cum este și Swing folosesc acest design pattern, de exemplu apăsarea unui buton generează un eveniment ce poate fi transmis mai multor listeners înregistrați acestuia (exemplu).

API-ul Java oferă clasele Observer și Observable care pot fi subclasate pentru a implementa propriile tipuri de obiecte ce trebuie monitorizate și observatorii acestora.

Command Pattern

Design pattern-ul Command încapsulează un apel cu tot cu parametri într-o clasă cu interfață generică. Acesta este Behavioral pentru ca modifică interacțiunea dintre componente, mai exact felul în care se efectuează apelurile.

Acest pattern este recomandat în următoarele cazuri:

  • pentru a ușura crearea de structuri de delegare, de callback, de apelare întârziată
  • pentru a reține lista de comenzi efectuate asupra obiectelor
    • accounting
    • liste de Undo, Rollback pentru tranzacții - suport pentru operatii reversibile (undoable operations)

Exemple de utilizare:

  • sisteme de logging, accounting pentru tranzacții
  • sisteme de undo (ex. editare imagini)
  • mecanism ordonat pentru delegare, apel întârziat, callback

Structura

Ideea principală este de a crea un obiect de tip Command care va reține parametrii pentru comandă. Comandantul reține o referință la comandă și nu la componenta comandată. Comanda propriu-zisă este anunțată obiectului Command (de către comandant) prin execuția unei metode specificate asupra lui. Obiectul Command este apoi responsabil de trimiterea (dispatch) comenzii către obiectele care o îndeplinesc (comandați).

Fig. 5: Diagrama de stări pentru CommandPattern

Tipuri de componente (roluri):

  • Invoker - comandantul
    • apelează acțiuni pe comenzi (invocă metode oferite de obiectele de tip Command)
    • poate menține, dacă e cazul, o listă a tutoror comenzilor aplicate pe obiectul (obiectele) comandate. Este necesară reținerea acestei liste de comenzi atunci când implementăm un comportament de undo/redo al comenzilor.
    • primește clase Command pe care să le invoce
  • Receiver - comandatul
    • este clasa asupra căreia se face apelul
    • conține implementarea efectivă a ceea ce se dorește executat
  • Command - obiectele pentru reprezentarea comenzilor implementează această interfață/o extind dacă este clasă abstractă
    • concrete command - ne referim la implementări/subclasele acesteia
    • de obicei conțin metode cu nume sugestiv pentru executarea acțiunii comenzii (e.g. execute()). Implementările acestora conțin apelul către clasa \textit{Receiver}.
    • în cazul implementării unor acțiuni undoable adăugăm metode pentru undo și/sau redo.
    • țin referințe către comandați (receivers) pentru a aplica/invoca acțiunea ce reprezintă acea comandă

În Java, se pot folosi atât interfețe cât și clase abstracte, pentru Command, depinzând de situație (e.g. clasă abstractă dacă știm sigur ca obiectele de tip Command nu mai extind și alte clase).

În diagrama din figure 5, comandantul este clasa Invoker care conține o referință la o instanță (command) a clasei (Command). Invoker va apela metoda abstractă execute() pentru a cere îndeplinirea comenzii. ConcreteCommand reprezintă o implementare a interfeței Command, iar în metoda execute() va apela metoda din Receiver corespunzătoare acelei acțiuni/comenzi.

Implementare

Diagrama de secvență din figure 6 prezintă apelurile în cadrul unei aplicație de editare a imaginilor, ce este structurată folosind pattern-ul Command. În cadrul acesteia, Receiver-ul este Image, iar comenzile BlurCommand și CropCommand modifică starea acesteia. Structurând aplicația în felul acesta, este foarte ușor de implementat un mecanism de undo/redo, fiind suficient să menținem în Invoker o listă cu obiectele de tip Command aplicate imaginii.

Fig. 6: Diagrama de secvență pentru comenzile de prelucrare a imaginilor

Pe wikipedia puteți analiza exemplul PressSwitch. Flow-ul pentru acesta este ilustrat în figure 7  Fig. 7: Diagrama de secvență pentru comenzile de aprindere/stingere a switch-ului

Exerciții

Tema comună a exercițiilor din acest laborator este realizarea unei aplicații care simulează un boardgame/cardgame, Dominion. Mecanica jocului se bazează pe trei tipuri de cărți: acțiuni, bani și victory cards (acestea stabilesc scorul). În fiecare tură fiecare jucător poate juca acțiuni și apoi cumpăra alte cărți cu banii pe care îi are atunci în mână.

Putem observa că atunci când dorim să implementăm elemente din joc, putem folosi diferite pattern-uri pentru a structura clasele, astfel acțiunile jucătorilor pot reprezenta comenzi (Command Pattern), jocul poate avea mai multe view-uri (un view pt scor, unul pt istoricul acțiunilor executate etc), deci Observer Pattern, folosim multe obiecte ce extind anumite tipuri și putem separa crearea de instanțe pentru acestea printr-un Factory.

  1. (1p) Creați două subclase ale clasei abstracte GameCommand, reprezentând acțiunile pe care le pot face jucătorii - play și buy, respectând detalii din comentariile din scheletul de cod.
  2. (1p) Creați o clasă PlayArea, practic unde se desfășoară jocul, aceasta oferind metoda public void play(GameCommand c), care execută o acțiune din joc (GameCommad).
  3. (2p) Modificați clasa PlayArea astfel încât să fie observabilă (implementează Observable)
    • menține o listă de observatori (Observer)
    • notifică observatorii atunci când un jucător execută ceva
    • evenimentele trimise observatorilor sunt de tip GameEvent
  4. (2p) Implementați și testați doi observatori:
    1. ScoreObserver - ține cont de numărul de puncte acumulate de fiecare jucător (valoare dată de victory cards cumpărate)
    2. GameLogger - afișează fiecare acțiune efectuată de jucători - jucarea unor action cards, punerea în joc a unor treasure cards și cumpărarea de cărți.
  5. (2p) Creați un obiect de tip singleton-factory care întoarce obiecte de tip GameCommand.
  6. (2p) Implementați un mecanism de undo/redo pe acțiunile jucătorului, cât timp este tura sa.
    • undo/redo se aplică pe comenzile date pe PlayArea, deci aici trebuie ținută o listă cu acestea, pentru a știi sa le facem undo.
    • PlayArea va oferi metodele undo și redo.
    • când se efectuează un undo trebuie notificați observatorii (puteți adăuga un nou tip de GameAction), iar observatorul Logger va afișa la ce s-a făcut undo, iar cel de Score va scoate scorul, dacă s-a facut undo la un buy de victory card (se va scădea scorul aferent Victory-card-ului implicat).
    • când se efectueză un redo notificați observatorii la fel cum îi notificați în metoda play.

Puteți juca Dominion și online.

Resurse

Referințe

arhiva/laboratoare/2013/lab11.txt · Last modified: 2014/12/05 14:27 by Adriana Draghici