Table of Contents

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:

Aspectele bonus urmărite sunt:

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

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.
  • Vă încurajăm să folosiți această versiune a pattern-ului Builder și să recurgeți la varianta mai complexă doar dacă aveți nevoie de mai mulți Builderi specifici.

Avantaje

Dezavantaje

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

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:

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:

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

Dezavantaje

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

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

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

Structura pattern-ului

Structura Strategy se bazează pe 3 elemente principale:

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

Dezavantaje

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

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

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

Probleme care apar:

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

Structura pattern-ului

Structura se bazează pe 4 elemente principale:

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

Dezavantaje

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)

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

2. Builder

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

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

2. Strategy

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

3. Command

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:

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 nu va fi contorizat ca 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:

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

Trebuie să implementați:

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:

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:

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:

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

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