Table of Contents

Design patterns - Factory, Strategy, Observer

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 - Factory, Strategy și Observer.

Introducere

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:

Design pattern-urile nu trebuie privite drept niște rețete care pot fi aplicate direct pentru a rezolva o problemă din design-ul aplicației, pentru că de multe ori pot complica inutil arhitectura. Trebuie întâi înțeles dacă este cazul să fie aplicat un anumit pattern, si de-abia apoi adaptat pentru situația respectivă. Este foarte probabil chiar să folosiți un pattern (sau o abordare foarte similară acestuia) fără să vă dați seama sau să îl numiți explicit. Ce e important de reținut după studierea acestor pattern-uri este un mod de a aborda o problemă de design.

În laboratoarele precedente au fost descrise patternurile Singleton și Visitor. Singleton este un pattern creațional, simplu, a cărui folosire este controversată (vedeți în laborator explicația cu anti-pattern). Visitor 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.

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 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:

Abstract Factory Pattern

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();
    }
}

Factory Method Pattern

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.

Fig. 2: 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 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.

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 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.

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 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.

Implementare

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.

Strategy Pattern

Design pattern-ul Strategy încapsulează algoritmii în clase ce oferă o anumită interfață de folosire, și pot fi selecționați la runtime. Ca și Command, acest pattern este behavioral pentru ca permite decuplarea unor clase ce oferă un anumit comportament și folosirea lor independentă în funcție de situația de la runtime.

Acest pattern este recomandat în cazul în care avem nevoie de un tip de algoritm (strategie) cu mai multe implementări posibile si dorim să alegem dinamic care algoritm îl folosim, fără a face sistemul prea strâns cuplat.

Exemple de utilizare:

Structură:

Denumirile uzuale în exemplele acestui pattern sunt: Strategy (pt interfață sau clasa abstractă), ConcreteStrategy pentru implementare, Context, clasa care folosește/execută strategiile.

Recomandare: Urmariti link-ul de la referinte catre postul de pe Stack Overflow care descrie necesitatea pattern-ului Strategy. Pe langa motivul evident de incapsulare a prelucrarilor/algoritmilor (care reprezinta strategiile efective), se prefera o anumita abordare: la runtime se verifica mai multe conditii si se decide asupra strategiei. Concret, folosind mecanismul de polimorfism dinamic, se foloseste o anumita instanta a tipului de strategie (ex. Strategy str = new CustomStrategy), care se paseaza in toate locurile unde este nevoie de Strategy. Practic, in acest fel, utilizatorii unei anumite strategii vor deveni agnostici in raport cu strategia utilizata, ea fiind instantiata intr-un loc anterior si putand fi gata utilizata. Ganditi-va la browserele care trebuie sa detecteze daca device-ul este PC, smartphone, tableta sau altceva si in functie de acest lucru sa randeze in mod diferit. Fiecare randare poate fi implementata ca o strategie, iar instantierea strategiei se va face intr-un punct, fiind mai apoi pasata in toate locurile unde ar trebui sa se tina cont de aceasta strategie.

Summary

Exerciții

În cadrul acestui laborator veți implementa o aplicație mock care primește date și le procesează.

Arhitectura ei va include patternurile Observer, Strategy și Factory și este descrisă în README-ul din arhiva dată cu scheletul de cod.

Task 1 - Observer pattern (3p)

Scheletul de cod vă oferă clasele MainApp (entry-point pentru testare), Utils și DataRepository.

DataRepository este obiectul observabil, care va primi date noi de la MainApp. Când acesta primește date noi va notifica observatorii săi: ConsoleLogger, ServerCommunicationController și DataAggregator.

Task 2 - Strategy pattern (3p)

Scheletul vă oferă interfața StepCountStrategy ce va fi implementată de către “algoritmii” de prelucrare a datelor: BasicStepCountStrategy și FilteredStepCountStrategy. Prima adună toate valorile primite, iar a doua le adună doar pe cele ce îndeplinesc niște condiții (să fie număr pozitiv și să nu fie o valoare prea mare (mai mult de 1000 de pași) venită prea curând de la ultimul update primit (în mai puțin de 1 minut).

Task 3 - Factory pattern (2p)

Creați clasa StepCountStrategyFactory care construiește instanțe de subclase ale StepCountStrategy.

Clasa factory va conține o metodă care să întoarcă o strategie. De exemplu: public StepCountStrategy createStrategy(String strategyType, DataRepository dataRepository). StrategyType poate fi un string care să indice tipul strategiei, vedeți în Utils stringurile definite deja.

Task 4 - Putting all together (2p)

Realizați TODO-urile din codul de test din MainApp și rulați. Puteți să vă adăugați propriile exemple de date pentru a verfica corectitudinea programului.

Resurse

Referințe