Administrativ
Laboratoare
Tema
Teste
Resurse utile
Alte resurse
Arhiva Teme
Administrativ
Laboratoare
Tema
Teste
Resurse utile
Alte resurse
Arhiva Teme
This is an old revision of the document!
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, Factory și Observer.
Design pattern-urile reprezintă soluții generale și reutilizabile ale unei probleme comune în design-ul software. Un design pattern este o descriere a soluției sau un template ce poate fi aplicat pentru rezolvarea problemei, nu o bucata de cod ce poate fi aplicata direct. În general pattern-urile orientate pe obiect arată relațiile și interacțiunile dintre clase sau obiecte, fără a specifica însă forma finală a claselor sau a obiectelor implicate.
Se consideră că există aproximativ 2000 de design patterns [2], iar principalul mod de a le clasifica este următorul:
O carte de referință pentru design patterns este “Design Patterns: Elements of Reusable Object-Oriented Software” [1], denumită și “Gang of Four” (GoF). Aceasta definește 23 de design patterns, foarte cunoscute și utilizate în prezent. Aplicațiile pot încorpora mai multe pattern-uri pentru a reprezenta legături dintre diverse componente (clase, module). În afară de GoF, și alți autori au adus în discuție pattern-uri orientate în special pentru aplicațiile enterprise și cele distribuite.
Pattern-urile GoF sunt clasificate în felul următor:
Î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.
Pattern-ul Singleton este utilizat pentru a restricționa numărul de instanțieri ale unei clase la un singur obiect, deci reprezintă o metodă de a folosi o singură instanță a unui obiect în aplicație.
Pattern-ul Singleton este util în următoarele cazuri:
Singleton este utilizat des în situații în care avem obiecte care trebuie accesate din mai multe locuri ale aplicației:
Exemple din API-ul Java:
Din punct de vedere al design-ului și testarii unei aplicații de multe ori se evită folosirea acestui pattern, în test-driven development fiind considerat un anti-pattern. A avea un obiect Singleton a carei referință o folosim peste tot prin aplicație introduce multe dependențe între clase și îngreunează testarea individuală a acestora.
In general, codul care folosește stări globale este mai dificil de testat pentru că implică o cuplare mai strânsă a claselor, și împiedică 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.
Aplicarea pattern-ului Singleton constă în implementarea unei metode ce permite crearea unei noi instanțe a clasei dacă aceasta nu există, și întoarcerea unei referințe către aceasta dacă există deja. În Java, 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. ##: 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 static Singleton instance = null; private Singleton {} public static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } ... }
instance
este privateDe ce Singleton și nu clase cu membri statici?
O clasă de tip Singleton poate fi extinsă, iar 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).
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 subclasa 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:
Fig. 1: Diagrama de clase pentru Abstract Factory
Codul următor corespunde diagramei din figure 1. Î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(); } }
Folosind pattern-ul Factory Method se poate defini o interfață pentru crearea unui obiect. Clientul care apelează metoda factory nu știe/nu îl interesează de ce subtip va fi la runtime instanța primită.
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.
Situația cea mai întâlnită în care se potrivește acest pattern este aceea când trebuie instanțiate 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
.
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
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ă.
Design Pattern-ul Observer definește o relație de dependență 1 la n î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:
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.
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 3, 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.
Fig. 3: Diagrama de clase pentru Observer Pattern
Subiect
Observator
View/ObservatorDerivat
Aceasta schemă 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 îl puteți găsi aici.
Un exemplu de implementare este exercițiul 2 de la laboratorul 5 (Clase interne). Observați diagrama de clase asociată acestuia.
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.
Pentru cod complex, concurent, cu evenimente asincrone, recomandăm RxJava, care folosește Observer pattern: github, exemplu.
Acest laborator și următorul au ca temă comună a exercițiilor realizarea unui joc controlat din consolă. Jocul constă dintr-o lume (aka hartă) în care se plimbă eroi de trei tipuri, colectează comori și se bat cu monștri. În acestă săptămână trebuie să implementați o parte din funcționalitățile jocului folosind patternurile Singleton, Factory și Observer, urmând ca la laboratorul următor să terminați implementarea folosind pattern-urile studiate atunci.
Detalii joc:
World
.Hero
și sunt de trei tipuri: Mage, Warrior, Priest.move
- se mută într-o zonă învecinatăattack
(de implementat în laboratorul următor)collect
- eroul ia comoara găsită în zona în care se aflăMain
.Hero
pentru fiecare tip de erou.toString
din Object
pentru fiecare erouattack
- deocamdată nu va omorî pe nimeni - puteți afișa ceva la consolăTreasureFactory
și HeroFactory
. Trebuie să implementăm două metode: createTreasure
în TreasureFactory
și o metodă de creare de eroi în HeroFactory
, fie ea createHero
.HeroFactory.createHero
, pasați ca parametru un Hero.Type
și un String
cu numele eroului și întoarceți un subtip de Hero
potrivit pentru tipul de erou.populateTreasures
din World
. Folosiți-vă de membrii map
și treasures
din World
. Trebuie să marcați pe hartă că aveți o comoară și să adăugați obiectul-comoară în lista de comori.add
din metoda main
. Trebuie să adăugați eroi acolo. Folosiți HeroFactory.createHero
.Observer
și Observable
în clasele potrivite.World
. Cazul start
din metoda main
.World
când eroii execută o acțiune. Aveți două TODO
-uri în clasa Hero
.World
trebuie să fie observabilă și să notifice pe observatorii săi atunci când a început jocul și când se schimbă ceva (e.g. s-a mutat un erou).