Î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 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.
Î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.
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 }
Î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.
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 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.
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); } }
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();
build() sau a metodei setter, și putem arunca IllegalStateException dacă există încălcări înainte de a crea o instanță a clasei.
Builder ceea ce poate crește liniile de cod și poate afecta performanța în unele cazuri foarte specifice.
Pizza să nu poată fi modificată după construire.private Optional<Integer> mushroomsCount = Optional.empty();
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.
Î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.
Observer are 4 componente principale:
Această structură decuplează complet obiectele între ele: subiectul nu știe cine îl ascultă, știe doar că trebuie să notifice.
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"); } }
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.
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 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.
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); } }
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.
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 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.
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 } }
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 |
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 |
Folosirea unui pattern în interiorul altui pattern este încurajată pentru a crea sisteme scalabile, de exemplu:
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:
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.
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.