This is an old revision of the document!


Laboratorul 9: Design Patterns II

Obiective

În laboratorul trecut am aprofundat câteva Design Pattern-uri populare. În cadrul acestui laborator vom studia alte câteva tipare eficiente și utilizate în industrie.

Aspectele urmărite sunt:

  • înțelegerea design pattern-urilor și a tipurilor acestora.
  • lucrul cu Builder.
  • lucrul cu Observer.
  • lucrul cu Strategy.
  • lucrul cu Command.

Aspectele bonus urmărite sunt:

  • definirea unor cazuri mai avansate în care sunt folosite Design Pattern-urile.

Builder

Builder este un creational design pattern folosit când ai obiecte complexe, cu multe câmpuri opționale sau pași de inițializare diferiți. El separă construcția obiectului de reprezentarea lui, astfel încât același proces de construire poate genera obiecte diferite.

Problema reală pe care o rezolvă Builder

În Programarea Orientată pe Obiecte, cel mai adesea avem clase care dețin unele date pe care le setăm și le accesăm ulterior. Crearea instanțelor unor astfel de clase ar putea fi uneori cam laborioasă. Să luăm în considerare următoarea clasă de Pizza

public class Pizza {
    private String pizzaSize;
    private int cheeseCount;
    private int pepperoniCount;
    private int hamCount;
 
    // constructor, getters, setters
}

O clasă foarte simplă la prima vedere însă lucrurile se complică pe masură ce vom crea acest obiect. Oricare pizza va avea o dimensiune, cu toate acestea, atunci când vine vorba de topping-uri, unele sau toate pot fi prezente sau deloc, prin urmare, unele dintre proprietățile clasei noastre sunt opționale, iar altele sunt obligatorii.

Ce se întâmplă însă când vrem să creăm obiecte cu o stare internă diferită?

Pizza p = new Pizza(“small”, 1, 0, 0)  // doar cu branza
Pizza p2 = new Pizza(“small”, 1, 1, 0) // branza si pepperoni

Soluția de mai sus nu este chiar foarte elegantă, dar funcționează. Însă, dacă am introduce un nou câmp în clasa Pizza, cum ar fi olives, ar trebui să schimbăm toate instanțele create astfel încât să folosească și constructorul care include și măsline, ceea ce nu este scalabil.

Prima iterație: supraîncărcarea constructorilor

Putem folosi supraîncărcarea constructorilor pentru a oferi mai multe posibilități de inițializare:

public class Pizza {
    private String pizzaSize; // mandatory
    private int cheeseCount; // optional
    private int pepperoniCount; // optional
    private int hamCount; // optional
 
    public Pizza(String pizzaSize) {
        this(pizzaSize, 0, 0, 0);
    }
 
    public Pizza(String pizzaSize, int cheeseCount) {
        this(pizzaSize, cheeseCount, 0, 0);
    }
 
    public Pizza(String pizzaSize, int cheeseCount, int pepperoniCount) {
        this(pizzaSize, cheeseCount, pepperoniCount, 0);
    }
 
    public Pizza(String pizzaSize, int cheeseCount, int pepperoniCount, int hamCount) {
        this.pizzaSize = pizzaSize;
        this.cheeseCount = cheeseCount;
        this.pepperoniCount = pepperoniCount;
        this.hamCount = hamCount;
    }
 
    // getters
}

Cu toate acestea, am rezolvat problema doar parțial. Nu putem, de exemplu, să creăm o pizza cu brânză și șuncă, dar fără pepperoni ca aceasta new Pizza(“small”, 1, 1), deoarece al treilea argument al constructorului este pepperoni.

Și aici vine a doua soluție comună - și mai multă supraîncărcare de constructori.

public class Pizza {
    private String pizzaSize; // mandatory
    private String crust; // mandatory
    private int cheeseCount; // optional
    private int pepperoniCount; // optional
    private int hamCount; // optional
    private int mushroomsCount; // optional
 
    public Pizza(String pizzaSize, String crust) {
        this(pizzaSize, crust, 0, 0, 0, 0);
    }
 
    public Pizza(String pizzaSize, String crust, int cheeseCount) {
        this(pizzaSize, crust, cheeseCount, 0, 0, 0);
    }
 
    public Pizza(String pizzaSize, String crust, int cheeseCount, int pepperoniCount) {
        this(pizzaSize, crust, cheeseCount, pepperoniCount, 0, 0);
    }
 
    public Pizza(String pizzaSize, String crust, int cheeseCount, int pepperoniCount, int hamCount) {
        this(pizzaSize, crust, cheeseCount, pepperoniCount, hamCount, 0);
    }
 
    public Pizza(String pizzaSize, String crust, int cheeseCount, int pepperoniCount, int hamCount, int mushroomsCount) {
        this.pizzaSize = pizzaSize;
        this.crust = crust;
        this.cheeseCount = cheeseCount;
        this.pepperoniCount = pepperoniCount;
        this.hamCount = hamCount;
        this.mushroomsCount = mushroomsCount;
    }
 
    // getters
}

Gândiți-vă ce se va întampla dacă se schimbă ordinea parametrilor. Acest lucru minor va strica funcționalitatea completă de creare a unei instanțe de Pizza.

În concluzie, modelul de constructori supraincarcati funcționează, dar este greu de menținut dacă se schimbă funcționalitatea și introducem noi parametri, numărul constructorilor va crește, de asemenea.

A doua iterație: folosirea de getters și setters

Putem folosi setteri pentru a stabili starea internă fără să fie nevoie să definim toate posibilitățile de constructori:

Pizza pizza = new Pizza();
 
pizza.setPizzaSize("small");
pizza.setCrust("thin");
pizza.setMushroomsCount(1);
pizza.setCheeseCount(1);
 
// do something with pizza

Modelul are însă dezavantaje grave. Construcția clasei este împărțită în apeluri multiple, prin urmare instanța poate fi într-o stare parțial construită / invalidă.

Structura pattern-ului

Structura se bazează pe 4 elemente principale:

  • Builder-ul abstract, care definește pașii necesari pentru a construi produsul (metode pentru configurarea fiecărei părți).
  • Builderii concreți, care implementează acești pași și construiesc variante specifice ale produsului.
  • Directorul, care controlează ordinea în care sunt apelați pașii de construire.
  • Produsul, obiectul final asamblat treptat de builder.

Această structură permite construirea de obiecte complexe pas cu pas, folosind aceeași secvență de construcție pentru produse diferite, fără a expune detalii interne despre cum este creat obiectul.

Exemplu complet

1. Produsul

public class Pizza {
    private String pizzaSize;
    private String crust;
    private int cheeseCount;
    private int pepperoniCount;
    private int hamCount;
    private int mushroomsCount;
 
    public void setPizzaSize(String pizzaSize) { this.pizzaSize = pizzaSize; }
    public void setCrust(String crust) { this.crust = crust; }
    public void setCheeseCount(int cheeseCount) { this.cheeseCount = cheeseCount; }
    public void setPepperoniCount(int pepperoniCount) { this.pepperoniCount = pepperoniCount; }
    public void setHamCount(int hamCount) { this.hamCount = hamCount; }
    public void setMushroomsCount(int mushroomsCount) { this.mushroomsCount = mushroomsCount; }
 
    @Override
    public String toString() {
        return "Pizza: size=" + pizzaSize + ", crust=" + crust +
            ", cheese=" + cheeseCount + ", pepperoni=" + pepperoniCount +
            ", ham=" + hamCount + ", mushrooms=" + mushroomsCount;
    }
}

2. Builder-ul abstract

public abstract class PizzaBuilder {
    protected Pizza pizza;
 
    public void createNewPizza() {
        pizza = new Pizza();
    }
 
    public Pizza getPizza() {
        return pizza;
    }
 
    public abstract void buildSize();
    public abstract void buildCrust();
    public abstract void buildCheese();
    public abstract void buildPepperoni();
    public abstract void buildHam();
    public abstract void buildMushrooms();
}

3. Builder-ul concret

public class PepperoniPizzaBuilder extends PizzaBuilder {
 
    @Override
    public void buildSize() {
        pizza.setPizzaSize("Large");
    }
 
    @Override
    public void buildCrust() {
        pizza.setCrust("Thin crust");
    }
 
    @Override
    public void buildCheese() {
        pizza.setCheeseCount(2);
    }
 
    @Override
    public void buildPepperoni() {
        pizza.setPepperoniCount(3);
    }
 
    @Override
    public void buildHam() {
        pizza.setHamCount(0);
    }
 
    @Override
    public void buildMushrooms() {
        pizza.setMushroomsCount(1);
    }
}

4. Directorul

public class PizzaDirector {
 
    private PizzaBuilder builder;
 
    public PizzaDirector(PizzaBuilder builder) {
        this.builder = builder;
    }
 
    public Pizza makePizza() {
        builder.createNewPizza();
        builder.buildSize();
        builder.buildCrust();
        builder.buildCheese();
        builder.buildPepperoni();
        builder.buildHam();
        builder.buildMushrooms();
        return builder.getPizza();
    }
}

5. Clientul

public class Main {
    public static void main(String[] args) {
 
        PizzaBuilder builder = new PepperoniPizzaBuilder();
        PizzaDirector director = new PizzaDirector(builder);
 
        Pizza pizza = director.makePizza();
 
        System.out.println(pizza);
    }
}

Exemplu complet varianta simplă

Există o variantă mai simplă a acestui pattern care folosește o clasă internă și pe care o veți folosi mai des în proiectele voastre:

public class Pizza {
    private String pizzaSize;
    private String crust;
    private int cheeseCount;
    private int pepperoniCount;
    private int hamCount;
    private int mushroomsCount;
 
    public static class Builder {
        private String pizzaSize; // mandatory
        private String crust; // mandatory
        private int cheeseCount = 0; // optional
        private int pepperoniCount = 0; // optional
        private int hamCount = 0; // optional
        private int mushroomsCount = 0; // optional
 
        public Builder(String pizzaSize, String crust) {
            this.pizzaSize = pizzaSize;
            this.crust = crust;
        }
 
        public Builder cheeseCount(int cheeseCount) {
            this.cheeseCount = cheeseCount;
            return this;
        }
 
        public Builder pepperoniCount(int pepperoniCount) {
            this.pepperoniCount = pepperoniCount;
            return this;
        }
 
        public Builder hamCount(int hamCount) {
            this.hamCount = hamCount;
            return this;
        }
 
        public Builder mushroomsCount(int mushroomsCount) {
            this.mushroomsCount = mushroomsCount;
            return this;
        }
 
        public Pizza build() {
            return new Pizza(this);
        }
    }
 
    private Pizza(Builder builder) {
        this.pizzaSize = builder.pizzaSize;
        this.crust = builder.crust;
        this.cheeseCount = builder.cheeseCount;
        this.pepperoniCount = builder.pepperoniCount;
        this.hamCount = builder.hamCount;
        this.mushroomsCount = builder.mushroomsCount;
    }
 
    // getters
}

Am făcut constructorul privat, astfel încât clasa noastră să nu poată fi instanțiată direct. În același timp am adăugat o clasă static Builder cu un constructor care are parametrii noștri obligatori pizzaSize și crust, metode de setare a parametrilor opționali și, în final, o metodă build() metoda care va returna o nouă instanță a clasei Pizza. Metodele setter returnează instanța de builder în sine, oferindu-ne astfel o interfață fluentă cu metoda de înlănțuire.

Pizza pizza = new Pizza.Builder("large", "thin")
    .cheeseCount(1)
    .pepperoniCount(1)
    .build();

Este mult mai ușor să scrieți și, mai important, să citiți acest cod. La fel ca în cazul constructorului, putem verifica parametrii trecuți pentru orice încălcare, cel mai adesea în cadrul metodei build() sau a metodei setter, și putem arunca IllegalStateException dacă există încălcări înainte de a crea o instanță a clasei.

Avantaje

  • Cod mult mai lizibil decât constructorii lungi.
  • Flexibilitate deoarece pașii de creare pot varia.
  • Evită stările intermediare invalide.
  • Ușor de extins și întreținut.
  • Perfect pentru obiecte complexe.

Dezavantaje

  • Mai multe clase pentru varianta mai complexă (overhead structural).
  • Poate deveni redundant pentru obiecte simple.
  • Necesită o clasă Builder ceea ce poate crește liniile de cod și poate afecta performanța în unele cazuri foarte specifice.

  • Varianta modernă a Builder-ului este să creezi obiecte complet imutabile, astfel încât Pizza să nu poată fi modificată după construire.
  • În loc de primitive pentru opționale, poți folosi Optional sau validări fluente:
    private Optional<Integer> mushroomsCount = Optional.empty();

Observer

Observer este un behavioral design pattern folosit atunci când ai un obiect (denumit Subject) care trebuie să notifice automat alte obiecte (Observeri) atunci când starea sa internă se schimbă, practic definând o relație 1:N.

Este extrem de util când vrei decuplare, adică subiectul să nu depindă de logica observatorilor.

Problema reală pe care o rezolvă Observer

În multe aplicații, avem un obiect central care deține o stare. Când starea se schimbă, alte componente trebuie să fie anunțate.

De exemplu:

  • o aplicație de bursă: când un preț se schimbă, graficul, notificările și istoricul trebuie actualizate.
  • un UI: când un slider se modifică, mai multe componente vizuale trebuie să se sincronizeze.
  • un joc: când sănătatea jucătorului se schimbă, bara de HP, sunetele și logica AI trebuie anunțate.

Fără Observer, am ajunge la cod de forma:

class Stock {
    private int price;
    ...
 
    void updatePrice(int price) {   
        this.price = price;
 
        chart.refresh();      // observer 1
        notification.send();  // observer 2
        history.log();        // observer 3
    }
public class Brocker {
    Stock stock = new Stock("NVDA");
 
    ...
 
    void updateMarket() {
        // Actualizare subject care declanșează acțiuni în observeri
        stock.updatePrice(190);
    }
}

În această situație, subiectul (Stock) se actualizează și va inițializa o serie de acțiuni către toți observatorii.

Problema este că toți observerii sunt apelați direct din logica ta → ceea ce înseamnă că trebuie să modifici codul subiectului ori de câte ori:

  • adaugi un observer nou,
  • scoți un observer,
  • schimbi ordinea notificărilor,
  • schimbi modul de update.

Ce facem dacă avem 20 de observatori?

Observer rezolvă asta printr-un principiu simplu: Subiectul nu mai știe cine îl observă. Doar păstrează o listă de observatori și îi notifică generic.

Structura pattern-ului

Observer are 4 componente principale:

  • Subject – menține o listă de observatori și le trimite notificări.
  • Observer (Interfața pentru observatori) – definește metoda update.
  • ConcreteSubject – Subject real care reține starea ce se schimbă.
  • ConcreteObservers – obiecte care reacționează la schimbări.

Această structură decuplează complet obiectele între ele: subiectul nu știe cine îl ascultă, știe doar că trebuie să notifice.

Exemplu complet

1. Interfața Observer

public interface Observer {
    void update(String status);
}

2. Subiectul abstract

import java.util.ArrayList;
import java.util.List;
 
public class Subject {
    private List<Observer> observers = new ArrayList<>();
 
    public void addObserver(Observer o) {
        observers.add(o);
    }
 
    public void removeObserver(Observer o) {
        observers.remove(o);
    }
 
    protected void notifyObservers(String status) {
        for (Observer o : observers) {
            o.update(status);
        }
    }
}

3. Subiectul concret

public class PizzaOrder extends Subject {
 
    private String status;
 
    public void setStatus(String status) {
        this.status = status;
        notifyObservers(status);
    }
 
    public String getStatus() {
        return status;
    }
}

4. Observatori concreți

public class SmsNotifier implements Observer {
    @Override
    public void update(String status) {
        System.out.println("SMS sent to client: order is " + status);
    }
}
public class KitchenDisplay implements Observer {
    @Override
    public void update(String status) {
        System.out.println("Kitchen display updated: order is " + status);
    }
}
public class TrackingApp implements Observer {
    @Override
    public void update(String status) {
        System.out.println("Tracking App: order is " + status);
    }
}

5. Clientul

public class Main {
    public static void main(String[] args) {
 
        PizzaOrder order = new PizzaOrder();
 
        order.addObserver(new SmsNotifier());
        order.addObserver(new KitchenDisplay());
        order.addObserver(new TrackingApp());
 
        order.setStatus("Preparing");
        order.setStatus("Baking");
        order.setStatus("Ready for pickup");
    }
}

  • 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.
  • Puteți face voi clasele Observer și Observable sau le puteți folosi pe cele din API-ul Java la teme și laboratoare.

Avantaje

  • Subjectul și Observerii sunt complet decuplați.
  • Poți adăuga observatori nelimitați fără să modifici Subjectul.
  • Foarte folosit în UI, jocuri, sisteme reactive.
  • Perfect pentru evenimente sau fluxuri de date.

Dezavantaje

  • Poate deveni greu de urmărit când ai mulți observatori care reacționează imprevizibil.
  • Risc de notificări în lanț (cascade effects).
  • Depanarea este uneori mai dificilă.

  • În proiecte mari, în loc să faceți voi lista de observatori manual, se folosesc librării precum Guava EventBus, Akka Event Stream, sau Spring ApplicationEvents.
  • În locul listei simple de observatori, în proiecte moderne se folosesc API-uri reactive: java.util.concurrent.Flow sau RxJava / Project Reactor, care permit backpressure și stream-uri de evenimente mai complexe.

Strategy

Strategy este un behavioral design pattern care permite definirea unei familii de algoritmi, fiind încapsulați și interschimbabili. Strategy permite schimbarea comportamentului unui obiect în timpul execuției fără a modifica codul clientului care îl folosește.

Problema reală pe care o rezolvă Strategy

Să presupunem că avem o aplicație care calculează discounturi pentru diferite tipuri de clienți:

  • clienții obișnuiți primesc 5% discount,
  • clienții premium primesc 10%,
  • clienții VIP primesc 20%.
public class ShoppingCart { 
    private String clientType;
 
    public ShoppingCart(String clientType) {
        this.clientType = clientType;
    }
 
    public double calculateDiscount(double price) {
        if(clientType.equals("regular")) {
            return price * 0.05;
        } else if(clientType.equals("premium")) {
            return price * 0.10;
        } else if(clientType.equals("vip")) {
            return price * 0.20;
        }
 
        return 0;
    }
}

Codul de mai sus este valid, dar ce ne facem dacă avem următoarea situație mai complexă în care folosim mai multe metode de plată:

  • Credit Card → verificări de securitate + comision 2%
  • PayPal → verificări externe + comision 3%
  • Bitcoin → verificări blockchain + comision variabil

În cazul în care urmăm exemplul codului de mai sus vom avea următoarele probleme:

  • Dacă adăugăm un nou tip de plată, trebuie să modificăm codul din ShoppingCart.
  • Dacă vrem să schimbăm formula de discount pentru un client existent, modificăm aceeași clasă.
  • Codul devine greu de întreținut și greu de testat.

Structura pattern-ului

Structura Strategy se bazează pe 3 elemente principale:

  • Interfața Strategy, care definește metoda comună pentru algoritmi.
  • Strategiile concrete, care implementează algoritmul specific.
  • Contextul, care folosește o strategie și poate să o schimbe la runtime.

Această structură permite adăugarea de noi algoritmi fără a modifica codul contextului, respectând principiul Open/Closed.

Exemplu complet

1. Interfața Strategy

public interface PaymentStrategy {
    boolean validate(String accountInfo);
    double calculateFee(double amount);
}

2. Strategii concrete

public class CreditCardPayment implements PaymentStrategy {
    @Override
    public boolean validate(String accountInfo) {
        System.out.println("Validating credit card: " + accountInfo);
        return accountInfo.length() == 16; // simplificat
    }
 
    @Override
    public double calculateFee(double amount) {
        return amount * 0.02;
    }
}
public class PayPalPayment implements PaymentStrategy {
    @Override
    public boolean validate(String accountInfo) {
        System.out.println("Validating PayPal account: " + accountInfo);
        return accountInfo.contains("@");
    }
 
    @Override
    public double calculateFee(double amount) {
        return amount * 0.03;
    }
}
public class BitcoinPayment implements PaymentStrategy {
    @Override
    public boolean validate(String accountInfo) {
        System.out.println("Validating Bitcoin wallet: " + accountInfo);
        return accountInfo.startsWith("1") || accountInfo.startsWith("3");
    }
 
    @Override
    public double calculateFee(double amount) {
        return Math.random() * 0.05 * amount; // comision variabil
    }
}

3. Contextul

public class PaymentProcessor {
    private PaymentStrategy paymentStrategy;
 
    public PaymentProcessor(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }
 
    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }
 
    public void processPayment(String accountInfo, double amount) {
        if(paymentStrategy.validate(accountInfo)) {
            double fee = paymentStrategy.calculateFee(amount);
            double total = amount + fee;
            System.out.println("Payment processed. Amount: " + amount + ", Fee: " + fee + ", Total: " + total);
        } else {
            System.out.println("Invalid payment information: " + accountInfo);
        }
    }
}

4. Clientul

public class Main {
    public static void main(String[] args) {
 
        PaymentProcessor processor = new PaymentProcessor(new CreditCardPayment());
        processor.processPayment("1234567812345678", 100);
 
        processor.setPaymentStrategy(new PayPalPayment());
        processor.processPayment("user@example.com", 200);
 
        processor.setPaymentStrategy(new BitcoinPayment());
        processor.processPayment("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 300);
    }
}

Avantaje

  • Algoritmi diferiți sunt încapsulați în clase separate → codul contextului nu se schimbă.
  • Se pot adăuga metode de plată noi fără a modifica PaymentProcessor.
  • Permite testare independentă a fiecărei strategii.
  • Comportamentul poate fi schimbat dinamic la runtime.

Dezavantaje

  • Crește numărul de clase (fiecare strategie concretă = o clasă nouă).
  • Contextul trebuie să gestioneze referința către strategie, ceea ce adaugă puțin overhead.
  • Poate fi redundant pentru cazuri simple, cu un singur algoritm.

În aplicații Spring sau Quarkus, Strategy este folosit în bean-uri, pentru a schimba comportamentul fără a modifica clientul.

Command

Command este un behavioral design pattern folosit pentru a încapsula o solicitare sau o acțiune ca un obiect. Acest pattern permite parametrizarea obiectelor cu diferite acțiuni, stocarea cererilor și posibilitatea de a le executa mai târziu, inclusiv să le anulezi sau să le refaci.

Problema reală pe care o rezolvă Command

Imagină-ți că dezvoltăm un editor de text complex, cu funcționalități precum:

  • Copy – copiază textul selectat în clipboard.
  • Paste – lipește textul din clipboard la poziția curentă.
  • Cut – taie textul selectat și îl pune în clipboard.
  • Undo – anulează ultima acțiune.
  • Redo – reface ultima acțiune anulată.

Fără Command pattern, codul ar putea arăta astfel:

editor.copy();
editor.paste();
editor.cut();
editor.undo();

Probleme care apar:

  • Clientul cunoaște toate metodele editorului – telecomanda sau butoanele din UI trebuie să știe exact ce metode să apeleze și în ce ordine.
  • Extensibilitate slabă – dacă vrem să adăugăm o acțiune nouă, de exemplu Bold sau Italic, trebuie să modificăm codul clientului pentru a apela noile metode.
  • Gestionarea undo/redo devine complicată – fiecare metodă are propria logică de inversare, iar clientul trebuie să cunoască starea internă a editorului pentru a implementa undo/redo corect.

Command pattern încapsulează fiecare acțiune ca un obiect separat. Fiecare comandă are metode precum execute() și undo(), astfel încât:

  • Clientul (UI-ul editorului) nu știe ce face comanda, doar o execută.
  • Adăugarea unei noi acțiuni nu implică modificarea codului clientului, trebuie doar să creăm o nouă comandă.
  • Istoricul de comenzi pentru undo/redo poate fi gestionat uniform, fără să știm detalii despre starea internă a editorului.

Structura pattern-ului

Structura se bazează pe 4 elemente principale:

  • Command-ul (interfața sau clasa abstractă) definește metoda execute().
  • Command-urile concrete implementează această interfață și definesc acțiuni specifice.
  • Invoker-ul, care apelează comanda fără să știe ce face efectiv.
  • Receiver-ul, obiectul care efectuează acțiunea reală.

Această structură permite stocarea, execuția și inversarea acțiunilor fără a expune detaliile interne ale obiectelor implicate.

Exemplu complet

1. Receiver-urile

public class TV {
    public void turnOn() {
        System.out.println("TV is ON");
    }
 
    public void turnOff() {
        System.out.println("TV is OFF");
    }
}
public class MusicSystem {
    public void playMusic() {
        System.out.println("Music is playing");
    }
 
    public void stopMusic() {
        System.out.println("Music stopped");
    }
}
public class Lights {
    public void dim() {
        System.out.println("Lights are dimmed");
    }
 
    public void bright() {
        System.out.println("Lights are bright");
    }
}

2. Interfața Command

public interface Command {
    void execute();
    void undo();
}

3. Command-uri concrete

public class TVOnCommand implements Command {
    private TV tv;
 
    public TVOnCommand(TV tv) {
        this.tv = tv;
    }
 
    @Override
    public void execute() {
        tv.turnOn();
    }
 
    @Override
    public void undo() {
        tv.turnOff();
    }
}
public class MusicPlayCommand implements Command {
    private MusicSystem music;
 
    public MusicPlayCommand(MusicSystem music) {
        this.music = music;
    }
 
    @Override
    public void execute() {
        music.playMusic();
    }
 
    @Override
    public void undo() {
        music.stopMusic();
    }
}
public class LightsDimCommand implements Command {
    private Lights lights;
 
    public LightsDimCommand(Lights lights) {
        this.lights = lights;
    }
 
    @Override
    public void execute() {
        lights.dim();
    }
 
    @Override
    public void undo() {
        lights.bright();
    }
}

4. Invoker-ul

import java.util.ArrayDeque;
import java.util.Deque;
 
public class RemoteControl {
    private Command slot;
    private Deque<Command> history = new ArrayDeque<>();
 
    public void setCommand(Command command) {
        this.slot = command;
    }
 
    public void pressButton() {
        slot.execute();
        history.push(slot);  // adaugă la începutul deque-ului
    }
 
    public void pressUndo() {
        if (!history.isEmpty()) {
            Command last = history.pop();  // elimină și returnează primul element
            last.undo();
        }
    }
}

5. Clientul

public class Main {
    public static void main(String[] args) {
        TV tv = new TV();
        MusicSystem music = new MusicSystem();
        Lights lights = new Lights();
 
        Command tvOn = new TVOnCommand(tv);
        Command musicPlay = new MusicPlayCommand(music);
        Command lightsDim = new LightsDimCommand(lights);
 
        RemoteControl remote = new RemoteControl();
 
        remote.setCommand(tvOn);
        remote.pressButton();   // TV is ON
 
        remote.setCommand(musicPlay);
        remote.pressButton();   // Music is playing
 
        remote.setCommand(lightsDim);
        remote.pressButton();   // Lights are dimmed
 
        // Undo ultimele acțiuni
        remote.pressUndo();     // Lights are bright
        remote.pressUndo();     // Music stopped
        remote.pressUndo();     // TV is OFF
    }
}

Avantaje

  • Fiecare acțiune e încapsulată ca un obiect → clientul (telecomanda) nu cunoaște detaliile implementării.
  • Permite undo pentru fiecare comandă, chiar și pentru acțiuni diferite.
  • Ușor de extins cu noi dispozitive și comenzi fără a modifica telecomanda sau clientul.
  • Poate stoca istoric de comenzi, permite programare și execuție la momentul dorit.

Dezavantaje

  • Mai multe clase pentru fiecare comandă → overhead structural.
  • Poate fi prea complex pentru scenarii simple (ex: doar on/off).

Pattern-ul Command este foarte folosit în aplicații enterprise moderne cu RabbitMQ, Kafka sau task queues, unde „comanda” devine un mesaj.

Tips & Tricks Design Patterns

Comparare între Design Patterns

Factory vs. Builder

Atât Factory cât și Builder sunt pattern-uri de creare (creational design patterns), dar rezolvă probleme diferite.

1. Factory (Factory Method / Abstract Factory)

  • Este folosit pentru crearea de obiecte simple sau familia de obiecte.
  • Se concentrează pe alegerea tipului de obiect care trebuie creat, fără a expune logica de creare clientului.
  • Constructorul sau metoda factory returnează obiectul complet creat.
  • Este potrivit când toate obiectele sunt gata de utilizare imediat și nu necesită pași complexi de construire.

Exemplu simplu: crearea unui Transport (Truck sau Ship) sau a unei Pizza cu un tip fix de ingrediente.

2. Builder

  • Este folosit pentru crearea obiectelor complexe, cu mulți pași de construire sau cu parametri opționali.
  • Se concentrează pe pas cu pas construirea obiectului, separat de reprezentarea finală.
  • Poate avea un director care controlează ordinea pașilor de construire.
  • Este potrivit când obiectul poate avea mai multe variante, iar pașii de construire trebuie să fie flexibili sau verificați.

Exemplu: construirea unei Pizza cu dimensiune, tip crust, topping-uri opționale și combinații multiple.

Caracteristică Factory Builder
Obiecte create Obiecte simple sau familii de obiecte Obiecte complexe, cu pași multipli
Control asupra pașilor Nu Da, pas cu pas
Extensibilitate Alegerea tipului de obiect Permite variante complexe fără a modifica codul clientului
Când se folosește Când obiectul este gata de utilizare imediat Când obiectul are parametri opționali, pași de construire sau variante complexe

Visitor vs. Strategy vs. Command

Toate trei sunt pattern-uri comportamentale (behavioral patterns), dar au scopuri diferite.

1. Visitor

  • Este folosit pentru a adăuga operații peste o structură de obiecte existente fără a modifica clasele obiectelor.
  • Se concentrează pe separarea algoritmilor de structura obiectelor.
  • Fiecare obiect “acceptă” un visitor, care efectuează operația specifică.
  • Potrivit când ai o structură de obiecte stabilă și vrei să adaugi funcționalități noi fără să modifici clasele existente.

Exemplu: calcularea taxelor sau generarea de rapoarte pentru diferite tipuri de noduri într-un arbore de obiecte (Book, DVD, Magazine).

2. Strategy

  • Este folosit pentru a schimba comportamentul unui algoritm la runtime.
  • Se concentrează pe înlocuirea unei logici de execuție fără a modifica clasa care folosește algoritmul.
  • Permite interchangeable behaviors printr-o interfață comună.
  • Potrivit când același obiect poate folosi mai multe algoritmi în funcție de context.

Exemplu: diferite strategii de calcul al taxelor (StandardTax, ReducedTax), sortări (QuickSort, MergeSort) sau algoritmi de compresie.

3. Command

  • Este folosit pentru încapsularea unei acțiuni ca obiect.
  • Se concentrează pe executarea, programarea și undo/redo acțiunilor fără ca clientul să știe detalii.
  • Fiecare comandă are metode precum execute() și undo().
  • Potrivit când acțiunile trebuie să fie tratate uniform, stocate, executate în ordinea lor sau anulate.

Exemplu: telecomanda unui sistem de divertisment (TV, Music, Lights), editor de text cu copy/paste/undo/redo.

Caracteristică Visitor Strategy Command
Scop principal Adăugarea de operații fără a modifica obiectele Schimbarea comportamentului unui algoritm la runtime Încapsularea unei acțiuni ca obiect
Cine știe despre cine? Visitor-ul știe despre obiecte Contextul știe despre algoritm Invoker-ul știe despre comandă
Flexibilitate Adaugă ușor operații noi Schimbă algoritmi ușor Adaugă acțiuni sau undo/redo ușor
Când se folosește Structură stabilă de obiecte Obiecte cu comportamente variabile Gestionarea comenzilor, istoric, cozi

Putem reduce și mai mult comparația celor 3 pattern-uri astfel:

  • Relație 1:N - Strategy - Un obiect (1) poate avea mai multe prețuri (N).
  • Relație N:N - Visitor - Mai multe tiprui de noduri (N) pot avea mai mulți algoritmi de exportare (N).
  • Operații care necesită istoric (undo/redo/do) - Command - Pictează o formă (do), apasă CTRL + Z (undo), apasă SHIFT + CTRL + Z (redo).

Folosirea mai multor Design Patterns

Folosirea unui pattern în interiorul altui pattern este încurajată pentru a crea sisteme scalabile, de exemplu:

  • Singleton + Factory
  • Factory + Builder
  • Observer + Strategy
  • etc.

Deși este recomandat să combinați aceste pattern-uri este foarte important să nu creați un sistem mult prea complex pentru nevoia aplicației voastre. Folosind ideologia “Sometimes less is more” vă recomandăm să nu optimizați prematur proiectele voastre și să țineți cont de principiul KISS.

Pentru a vedea mai multe Design Patterns și pentru a le compara vă recomandăm să studiați catalogul de pe Refactoring Guru.

Exerciții

  • Exercițiile vor fi făcute pe platforma Devmind Code. Găsiți exercițiile din acest laborator în contestul aferent.
  • Vă recomandăm să copiați scheletul și să faceți exercițiile mai întâi în IntelliJ, deoarece acolo aveți acces la o serie de instrumente specifice unui IDE. După ce ați terminat exercițiile puteți să le copiați pe Devmind Code.

În cadrul acestui laborator, exercițiile valorează în total 18p. Pentru primirea punctajului maxim pe acest laborator, trebuie să acumulați 10p din rezolvarea exercițiilor, orice depășește 10p fiind contorizat ca și bonus.

Task 1 - Observer, Strategy, Factory (8p)

Part 1. (4p) - Strategy, Factory

În cadrul acestui exercițiu, dorim să implementăm un magazin, ce are disponibile mai multe modalități de plată pentru clienții săi:

  • folosind un voucher
  • IBAN-ul
  • un card pe care îl posedă

Cele 3 modalități de plată sunt reprezentate de clase ce implementează interfața PaymentStrategy.

Trebuie să implementați:

  • metoda pay din cadrul fiecărei metode de plată.
  • metoda getPaymentMethod, ce primește un client ca parametru și instanțiază o tranzacție folosind o anumită metodă de plată, aleasă random.

Part 2. (4p) - Observer

În acest context, subscriber-ul/observer-ul este clasa Person. Această clasă implementează interfața BalanceObserver. Clasa Shop, care este subiectul/publisher-ul care trebuie să implementeze interfața TransactionPublisher va avea rolul de a notifica un client când o tranzacție este efectuată.

Trebuie să implementați:

  • metoda update din cadrul clasei Person, ce preia notificarea faptului că un client are în cont mai puțini Lei decât o anumită limită, specifică fiecărui client în parte, printr-un mesaj adecvat (”$nume$ $prenume$, ramai fara bani, saracule!”);
  • metoda payBy din cadrul clasei Person, ce întoarce valoarea de adevăr a reușitei efectuării unei plăți (dacă e posibil, efectuează plata), iar în caz negativ afisează și un mesaj adecvat (”$nume$ $prenume$, pleaca de aici, saracule!”);
  • metoda createTransaction din cadrul clasei Shop, ce verifică dacă trebuie notificată persoana cu ajutorul metodei update, în urma unei tranzacții.

Task 2 - Builder pattern (2p)

a) Examinați câmpurile din skeletul clasei House care includ câteva facilități obligatorii în construcția unei case, spre exemplu:

  • locația construcției
  • numărul de etaje
  • camere
  • unele opționale pe care le poate selecta sau nu clientul, cum ar fi:
    • electrocasnice
    • piscină
    • panouri solare
    • securitate

Completați constructorul public și metoda toString.

b) În clasa HouseBuilder, completați câmpurile, constructorul și metodele de adăugare a facilităților opționale.

c) Finalizați metoda build și decomentați codul din Main pentru a putea testa corectitudinea funcționalității.

Task 3 - Command pattern (8p)

Implementați folosind patternul Command un editor de diagrame foarte simplificat. Scheletul de cod conține o parte din clase și câteva teste.

Componentele principale ale programului:

  • DiagramCanvas - reprezintă o diagramă care conține obiecte de tip DiagramComponent
  • DrawCommand - interfață pentru comenzile făcute asupra diagramei sau a componentelor acesteia
  • Invoker - primește comenzile și le execută
  • Client - entry-point-ul în program

(4p) Implementați 4 tipuri de comenzi, pentru următoarele acțiuni:

  • Draw rectangle - crează o DiagramComponent și o adaugă în DiagramCanvas
  • Resize - modifică width și height al unei DiagramComponent pe baza unui procent dat
  • Change color - modifică culoarea unei DiagramComponent
  • Change text - modifică textul unei DiagramComponent

Implementați pe Invoker metoda execute() care va executa comanda primită ca argument.

Comenzile primesc în constructor referința către DiagramCanvas și alte argumente necesare lor. De exemplu, comanda pentru schimbarea culorii trebuie sa primească și culoarea nouă și indexul componentei.

Pentru acest task nu este nevoie să implementați și metoda undo(), doar execute().

Comenzile implementează în afară de metodele interfeței și metoda toString() pentru a afișa comanda. Recomandăm folosirea IDE-ului pentru a o genera.

(4p) Implementați în comenzi și în Invoker mecanismul de undo/redo al comenzilor. Recomandăm în Invoker sa folosiți două structuri de date, una care să mențină comenzile efectuate, iar una pentru comenzile făcute undo. Metoda reset() de pe Invoker va avea ca scop resetarea tuturor membrilor acestuia.

Resurse și link-uri utile

poo-ca-cd/laboratoare/design-patterns-part-two.1764722591.txt.gz · Last modified: 2025/12/03 02:43 by florian_luis.micu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0