Table of Contents

Laboratorul 8: Design Patterns I

Obiective

Pe parcursul laboratoarelor și temelor ați folosit entități și v-ați definit propriile tipuri de interacțiuni. În cadrul acestui laborator vom aprofunda câteva tipare eficiente și utilizate în industrie.

Aspectele urmărite sunt:

Aspectele bonus urmărite sunt:

  • În acest laborator există mai multe secțiuni marcate [Optional]. Aceste secțiuni cuprind informații bonus care vă pot fi prezentate în timpul laboratorului sau pe care le puteți aprofunda în afara acestuia, ele nefiind necesare pentru laboratoarele viitoare sau pentru teme.
  • De asemenea, veți întâlni câteva secțiuni marcate [Nice to know]. Vă recomandăm ca acestea să aibă prioritate în parcurgerea secțiunilor de tip [Optional], deoarece vă pot oferi informații bonus care să fie și foarte probabil utile pentru teme sau laboratoare viitoare.

Ce sunt design pattern-urile?

Design pattern-urile reprezintă soluții generale și reutilizabile pentru probleme comune de design software. Ele nu oferă cod gata de folosit, ci șabloane și relații între clase și obiecte pentru a rezolva probleme recurente.

Design pattern-urile se împart în 3 familii:

  1. Creational Patterns — despre cum se creează obiectele
  2. Structural Patterns — despre cum combinăm obiectele în structuri mari
  3. Behavioral Patterns — despre cum comunică și cooperează obiectele

  • În următoarele secțiuni vom prezenta câteva design pattern-uri și este important să știm că aceste pattern-uri pot fi combinate pentru a crea sisteme complexe.
  • Toate design pattern-urile au fost definite în faimoasa carte The Gang of Four și sunt agnostice din punct de vedere al limbajului, adică aceste principii pot fi aplicate pentru orice limbaj OOP.
  • Design pattern-urile pot fi aplicate și la scară mai mare, de exemplu în cloud computing.

În următorul laborator vom explora câteva design pattern-uri suplimentare (Builder, Observer, Strategy, Command), fără a putea acoperi însă întreaga gamă existentă.

Pattern-urile incluse în aceste două laboratoare sunt printre cele mai frecvent utilizate, însă vă recomandăm să continuați studiul și asupra altora pentru o înțelegere completă.

🔒 Singleton

Singleton este un design pattern creational care se asigură că o clasă are:

  1. o singură instanță în toată aplicația
  2. o modalitate globală de acces la acea instanță

Practic, atunci când există ceva ce are sens să existe o singură dată (un logger, un config manager, o conexiune, un registry global) folosim Singleton pentru a evita crearea mai multor obiecte.

Singleton ≈ o “firmă” care trebuie să aibă un singur director general. Poți suna la firmă oricând, dar șeful e mereu același.

Problema pe care o rezolvă Singleton

Să ne imaginăm o aplicație care folosește o clasă Config pentru a încărca setări importante, citite dintr-un fișier config.properties.

De asemenea, avem trei clase ale aplicației unde fiecare își ia configurația în felul ei pentru a citi anumite proprietăți din fișier:

public class LoginPage {
    void render() {
        Config config = new Config();  // încărcare 1
        System.out.println(config.get("page.theme"));
    }
}
 
public class DashboardPage {
    void render() {
        Config config = new Config();  // încărcare 2
        System.out.println(config.get("panel.size"));
    }
}
 
public class ReportGenerator {
    void run() {
        Config config = new Config();  // încărcare 3
        System.out.println(config.get("report.max_length"));
    }
}

Apoi aplicația rulează:

new LoginPage().render();
new DashboardPage().render();
new ReportGenerator().run();

Rezultat: Fișierul config.json este citit de trei ori, deși conținutul este identic.

Problemele care apar imediat

1. Multiplicarea inutilă a obiectelor

Ajungem să avem 3, 5 sau 20 de instanțe ale aceleiași configurații, toate identice.

2. Cost mare de inițializare

Fișierul poate avea multe date în el, astfel citirea lui devine scumpă. De ce să o facem de 20 de ori?

3. Inconsistență

Dacă fișierul se schimbă între două instanțieri, unele clase lucrează cu valori noi, altele cu valori vechi, ceea ce rezultă în bug-uri greu de depistat.

4. Lipsa controlului asupra ciclului de viață

Orice developer poate scrie:

new Config();

și creează o nouă instanță fără să știe.

5. Avem nevoie de acces global, dar „sigur”

Vrem să putem accesa configurația de oriunde, dar fără riscurile variabilelor globale care pot fi suprascrise accidental, ceea ce poate afecta clasele care folosesc această variabilă globală.

Structura pattern-ului

Singleton rezolvă problema printr-un mecanism clar:

1. Constructor privat

2. Metodă statică getInstance()

3. Instanță statică stocată intern

4. Oferă un punct global de acces

Astfel, clasa își controlează propriul ciclu de viață și asigură existența unei singure instanțe, oriunde în program.

Implementare clasică (Eager Initialization)

Una dintre cele mai simple și sigure implementări, instanța este creată când e încărcată clasa, nu când este folosită.

public class Singleton { 
    // Instanțiem direct obiectul la declarare
    private static final Singleton INSTANCE = new Singleton();
 
    private Singleton() { }
 
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

Avantaje:

Dezavantaje:

Implementare Lazy Initialization

Instanță este creată doar când este necesară.

public class Singleton { 
    private static Singleton instance;
 
    private Singleton() { }
 
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // creat DOAR la prima utilizare
        }
 
        return instance;
    }
}

Problemă:

Se poate rezolva cu:

Pentru laborator și teme varianta simplă este suficientă pentru că nu folosim cod multi-threaded.

[Optional] Singleton cu Enum

Aceasta este considerată cea mai sigură implementare a unui Singleton în Java:

public enum Singleton { 
    INSTANCE;
 
    public void doSomething() {
        System.out.println("Working...");
    }
}

Avantaje:

Avantaje

Avantaj De ce e util
O singură instanță Evită duplicarea resurselor
Acces global Cod simplu, accesibil de oriunde
Inițializare controlată Poate încărca resurse la nevoie
Bun pentru resurse costisitoare DB connection manager, config, cache

Dezavantaje

Problemă Explicație
Violare SRP Clasa controlează logica + ciclul de viață
Global state Duce la dependențe ascunse, greu de urmărit
Testare dificilă Starea globală afectează testele
Maschează design prost Devine „soluția rapidă” la orice, dar greșit

Din aceste motive Singleton este considerat un anti-pattern, adică ar trebui să nu fie folosit excesiv.

De ce folosim Singleton dacă este un anti-pattern?

O alternativă foarte bună la Singleton este agregarea: creezi o singură instanță a clasei (ex: Config) și o transmiți în constructorul claselor care au nevoie de ea.

Avantaje:

Există totuși câteva probleme cu această abordare:

1. Pasarea responsabilității

Dacă folosim agregare, trebuie ca cineva să gestioneze:

De obicei, acest „cineva” este un container de dependențe sau o clasă compoziție (ex. App).

Fără un framework (Spring, Micronaut, Guice), trebuie să gestionezi tu manual acest „compozitor”.

În aplicațiile mai mari, asta devine rapid greu de întreținut și poate deveni o sursă de bug-uri (ex. un programator reinițializează referința care a fost pasată, deci clasele care o foloseau vor lucra cu obiectul vechi).

2. Se pot crea oricâte obiecte

Agregarea nu garantează unicitatea.

Dacă un developer scrie:

new Config();

nimic nu îl oprește.

Putem ajunge, accidental, în aceeași situație pentru care Singleton a fost inventat:

Singleton, în schimb, controlează strict numărul instanțelor.

Concluzie:

  • Agregarea este, în general, soluția preferată, mai ales în aplicații mari, testabile, modularizate, și în orice framework modern de DI.
  • Singleton este util doar când unicitatea este esențială și imposibil de delegat altcuiva, iar aplicația este mică sau nu folosește DI.

Vom vorbi despre testare pe larg în următoarele laboratore.

Use case practic — ConfigManager

Situație comună într-o aplicație reală:

public class ConfigManager {
    private static ConfigManager instance;
    private Properties props;
 
    private ConfigManager() {
        props = new Properties();
        try(FileInputStream input = new FileInputStream("app.config")) {
            props.load(input);
        } catch (IOException e) {
            throw new RuntimeException("Cannot load config");
        }
    }
 
    public static ConfigManager getInstance() {
        if (instance == null) {
            instance = new ConfigManager();
        }
        return instance;
    }
 
    public String get(String key) {
        return props.getProperty(key);
    }
}

Utilizare:

String dbUrl = ConfigManager.getInstance().get("db.url");

Tot codul din aplicație citește configurația din aceeași instanță, fără duplicări, fără costuri mari la fiecare apel.

🏭 Factory

Factory Method

Factory Method este un pattern creational care rezolvă problema creării obiectelor atunci când nu vrem ca logica principală a aplicației să depindă de clasele concrete. În loc să instanțiem obiecte cu new direct în codul client, delegăm această responsabilitate unei metode “fabrică”. Această metodă poate fi suprascrisă de subclase pentru a controla tipul exact de obiect construit.

Scopul este să obținem un cod flexibil, ușor de extins și structurat astfel încât modificările să nu afecteze restul sistemului. Factory Method păstrează codul curat și respectă principiile OCP (Open-closed Principle) și DIP (Dependency Inversion Principle).

Problema reală pe care o rezolvă Factory Method

Presupune că ai o aplicație de livrare care trebuie să poată trimite pachete fie pe uscat (camion), fie pe mare (navă). Codul client (procesul de planificare) nu ar trebui să depindă de tipul concret de transport.

Dacă ai scrie ceva de genul:

if (type.equals("road")) {
    return new Truck();
} else {
    return new Ship();
}

codul ar deveni încărcat, greu de întreținut și greu de extins.

Factory Method evită această problemă prin mutarea instanțierii în subclase.

Structura pattern-ului

Structura se bazează pe 3 elemente:

Această structură permite extinderea aplicației fără a modifica codul deja scris, doar adăugăm noi creatori.

Exemplu complet

1. Produsul

interface Transport {
    void deliver();
}

2. Implementări concrete

class Truck implements Transport {
    @Override
    public void deliver() {
        System.out.println("Delivery by truck via road.");
    }
}
 
class Ship implements Transport {
    @Override
    public void deliver() {
        System.out.println("Delivery by ship on sea.");
    }
}

3. Creatorul abstract cu factory method

abstract class Logistics {
    public abstract Transport createTransport();
 
    public void planDelivery() {
        Transport t = createTransport();
        t.deliver();
    }
}

4. Creatorii concreți

class RoadLogistics extends Logistics {
    @Override
    public Transport createTransport() {
        return new Truck();
    }
}
 
class SeaLogistics extends Logistics {
    @Override
    public Transport createTransport() {
        return new Ship();
    }
}

5. Codul client

public class Main {
    public static void main(String[] args) {
        Logistics road = new RoadLogistics();
        road.planDelivery();
 
        Logistics sea = new SeaLogistics();
        sea.planDelivery();
    }
}

Avantaje importante

Factory Method oferă flexibilitate: fiecare subclasă de fabrică decide ce obiecte creează, fără ca restul aplicației să fie afectat. Clientul folosește doar metoda create() și nu trebuie să știe cum sau ce tip concret este instanțiat.

Acest pattern devine valoros atunci când sistemul trebuie extins cu noi tipuri de produse. Adăugarea unui nou tip implică doar crearea unei noi fabrici sau a unei noi implementări a metodei, fără modificarea codului existent, ceea ce îmbunătățește izolarea, testarea și mentenanța.

Abstract Factory

Abstract Factory este un pattern creational folosit atunci când avem nevoie să creem familii întregi de obiecte compatibile între ele, fără ca logica clientului să depindă de implementările concrete.

În loc să creem „un singur obiect” ca în Factory Method, Abstract Factory creează grupuri de obiecte care trebuie să funcționeze împreună.

Problema reală pe care o rezolvă Abstract Factory

Imaginează-ți că scrii o aplicație grafică care poate rula pe Windows și pe macOS. Fiecare platformă are propria estetică, propriul set de widget-uri și propriul comportament.

Putem asuma că vrem să creăm mai multe tipuri de butoane (ex. checkbox, clickable button, dropdown etc.). Dacă am combina manual obiectele, am risca să creem combinații incompatibile:

Abstract Factory garantează consistența interfeței și separă clar logica aplicației de logica de randare pentru exemplul dat.

Structura pattern-ului

Patternul are 4 componente principale:

Exemplu complet

1. Produsele abstracte

interface Button {
    void paint();
}
 
interface Checkbox {
    void render();
}

2. Implementări concrete — Windows

class WindowsButton implements Button {
    @Override
    public void paint() {
        System.out.println("Painting Windows button.");
    }
}
 
class WindowsCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Rendering Windows checkbox.");
    }
}

3. Implementări concrete — macOS

class MacButton implements Button {
    @Override
    public void paint() {
        System.out.println("Painting macOS button.");
    }
}
 
class MacCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Rendering macOS checkbox.");
    }
}

4. Fabrica abstractă

interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

5. Fabrica Windows

class WindowsFactory implements GUIFactory {
    public Button createButton() { return new WindowsButton(); }
    public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}

6. Fabrica macOS

class MacFactory implements GUIFactory {
    public Button createButton() { return new MacButton(); }
    public Checkbox createCheckbox() { return new MacCheckbox(); }
}

7. Codul client

class Application {
    private final Button button;
    private final Checkbox checkbox;
 
    public Application(GUIFactory factory) {
        this.button = factory.createButton();
        this.checkbox = factory.createCheckbox();
    }
 
    public void start() {
        button.paint();
        checkbox.render();
    }
}

Avantaje importante

Principalul câștig este consistența: dacă alegem WindowsFactory, toate obiectele sunt Windows; dacă alegem MacFactory, totul rămâne macOS. Codul client nici nu știe ce platformă folosește.

Acest pattern devine esențial în aplicații mari, cross-platform sau cu teme diferite, când coerența între componente este critică.

Factory Method vs Abstract Factory

🚶‍♂️ Visitor

Visitor este un behavioural design pattern care permite adăugarea de funcționalități la o ierarhie de clase fără a modifica structura acestora.

Problemele pe care le rezolvă Visitor

Să ne imaginăm o aplicație care gestionează noduri într-o structură geografică sau industrială: orașe (City), obiective turistice (SightSeeing) și facilități industriale (Industry). Dorim să procesăm aceste noduri diferit, de exemplu să le exportăm în diferite formate sau să le afișăm într-un anumit fel.

Prima iteratie: instanceof și metode externe

Prima soluție folosește o clasă ExportVisitor cu metode separate pentru fiecare tip de nod. Clientul trebuie să testeze tipul obiectului cu instanceof și să apeleze metoda corespunzătoare.

interface Node {}
 
class City implements Node {}
class Industry implements Node {}
class SightSeeing implements Node {}
 
// Clasa care conține operațiile de procesare
class ExportVisitor {
    void doForCity(City c) { System.out.println("Export City"); }
    void doForIndustry(Industry f) { System.out.println("Export Industry"); }
    void doForSightSeeing(SightSeeing ss) { System.out.println("Export SightSeeing"); }
}
 
class Main {
    public static void main(String[] args) {
        List<Node> nodes = new ArrayList<>();
        nodes.add(new City());
        nodes.add(new Industry());
        nodes.add(new SightSeeing());
 
        ExportVisitor exportVisitor = new ExportVisitor();
 
        // NOT GOOD: folosim instanceof pentru a selecta metoda corectă
        for (Node node: nodes) {
            if (node instanceof City) {
                exportVisitor.doForCity((City) node);
            } else if (node instanceof Industry) {
                exportVisitor.doForIndustry((Industry) node);
            } else if (node instanceof SightSeeing) {
                exportVisitor.doForSightSeeing((SightSeeing) node);
            }
        }
    }
}

Probleme:

A doua iterație: Overloading static

Încercăm să folosim supraîncărcare, definind o metodă export pentru fiecare tip de nod în clasa Exporter. Teoretic, aceasta ar trebui să aleagă metoda corectă automat.

interface Node {
    void show();
}
 
class City implements Node {
    @Override
    public void show() { System.out.println("City display"); }
}
 
class Industry implements Node {
    @Override
    public void show() { System.out.println("Industry display"); }
}
 
class SightSeeing implements Node {
    @Override
    public void show() { System.out.println("SightSeeing display"); }
}
 
// Clasa care încearcă să folosească overloading
class Exporter {
    void export(Node node) { System.out.println("Export Node"); }
    void export(City city) { System.out.println("Export City"); }
    void export(Industry industry) { System.out.println("Export Industry"); }
    void export(SightSeeing sightSeeing) { System.out.println("Export SightSeeing"); }
}
 
class Main {
    public static void main(String[] args) {
        Exporter exporter = new Exporter();
 
        Node city = new City();
        Node industry = new Industry();
        Node sightseeing = new SightSeeing();
 
        // Problema apare aici: overloading se rezolvă la compile-time
        exporter.export(city);       // Va apela export(Node) !  
        exporter.export(industry);   // Va apela export(Node)
        exporter.export(sightseeing);// Va apela export(Node)
    }
}

Probleme:

Polimorfismul dinamic nu ar avea problema de mai devreme. De exemplu, aceste apeluri sunt rezolvate corect la runtime:

city.show(); // va apela metoda suprascrisă din City
industry.show(); // va apela metoda suprascrisă din Industry

Însă această soluție permite un singur comportament de afișare per clasă. Dacă vrem să afișăm același obiect în mai multe formate (TEXT, JSON, XML), nu putem suprascrie show() de trei ori. Ar trebui să delegăm afișarea către clase externe (ExporterText, ExporterJson, ExporterXml), iar dacă folosim overloading pentru acestea, revenim la aceeași problemă: overloading-ul se decide la compile-time, ignorând tipul real al obiectului.

Concluzie:

Avem nevoie de un mecanism care să determine metoda corectă la runtime, în funcție de tipul real al obiectului. Aceasta se face prin double dispatch, ceea ce duce la iterația 3 (Visitor).

Structura pattern-ului

Componentă Rol Exemplu
Visitor Interfață care definește metodele de vizitare NodeVisitor cu visit(City), visit(Industry) etc.
ConcreteVisitor Implementarea operațiilor pentru fiecare tip de obiect NodePrintVisitor, NodeJsonVisitor
Visitable (Node) Interfața pentru obiecte care pot fi vizitate Node cu accept(Visitor v)
ConcreteVisitable Implementarea obiectelor vizitabile City, Industry, SightSeeing

Soluție – Visitor cu double dispatch

Single Dispatch

Când încercăm să apelăm metode diferite în funcție de tipul unui obiect, avem două mecanisme în Java:

  1. Single dispatch – metoda apelată depinde doar de tipul obiectului pe care îl apelăm (runtime).
  2. Overloading static (compile-time) – metoda este aleasă după tipul referinței cunoscut la compilare.

Reluăm exemplul de problemă de mai sus:

class Exporter {
    void export(Node node) { System.out.println("Export Node"); }
    void export(City city) { System.out.println("Export City"); }
}
 
Node city = new City();
Exporter exporter = new Exporter();
exporter.export(city); // Apelează export(Node) !!!

Explicație:

Aceasta este situația clasică unde single dispatch și overloading nu sunt suficiente. Avem nevoie de double dispatch.

Double Dispatch

Prin double dispatch, vrem ca metoda apelată să depindă de tipul obiectului vizitabil și de tipul vizitatorului.

  1. Primul “dispatch”: apelul metodei accept(visitor) pe obiectul vizitabil.
  2. Al doilea “dispatch”: apelul visit(this) din interiorul accept, unde this este tipul real al obiectului.

Exemplu simplu:

interface Node {
    void accept(Visitor v);
}
 
class City implements Node {
    @Override
    public void accept(Visitor v) {
        v.visit(this); // tipul real al obiectului City este acum cunoscut
    }
}
 
interface Visitor {
    void visit(City city);
}

Astfel, metoda corectă este aleasă la runtime, fără instanceof și fără cod duplicat.

A treia iterație: Visitor

1. Definirea nodurilor vizitabile

interface Node {
    void accept(Visitor v);
}
 
class City implements Node {
    @Override
    public void accept(Visitor v) { v.visit(this); }
}
 
class Industry implements Node {
    @Override
    public void accept(Visitor v) { v.visit(this); }
}
 
class SightSeeing implements Node {
    @Override
    public void accept(Visitor v) { v.visit(this); }
}

2. Definirea vizitatorului

interface Visitor {
    void visit(City city);
    void visit(Industry industry);
    void visit(SightSeeing sightSeeing);
}

3. Implementarea unui vizitator concret

class NodeVisitor implements Visitor {
    public void visit(City city) { System.out.println("Export City"); }
    public void visit(Industry industry) { System.out.println("Export Industry"); }
    public void visit(SightSeeing sightSeeing) { System.out.println("Export SightSeeing"); }
}

4. Utilizarea pattern-ului

List<Node> nodes = new ArrayList<>();
nodes.add(new City());
nodes.add(new Industry());
nodes.add(new SightSeeing());
 
Visitor visitor = new NodeVisitor();
 
for (Node node : nodes) {
    node.accept(visitor); // double dispatch aplicat
}

Avantaje

Dezavantaje

Pattern-ul Visitor este util când:

  1. Se dorește prelucrarea unei structuri complexe, cu obiecte de tipuri diferite.
  2. Operațiile se adaugă mai des decât clasele obiectelor.
  3. Dorim să separăm algoritmii de structura de date, păstrând clasele vizitabile simple.

TL;DR: Folosim Visitor când vrem să avem mai mulți algoritmi (ex. ExporterPDF, ExporterXML, ExporterJSON) pentru mai multe tipuri de entități (ex. SeightSeeing, Industry, City).

🛡️ [Nice to know] Proxy

Proxy este un structural design pattern care oferă un înlocuitor sau un intermediar pentru un alt obiect, controlând accesul la acesta. Obiectul proxy poate adăuga logica suplimentară, cum ar fi caching, logging, securitate sau încărcare întârziată (lazy loading), fără a modifica obiectul real.

Problema pe care o rezolvă Proxy

Imaginează-ți o aplicație care lucrează cu un obiect costisitor de creat, de exemplu un fișier mare, o conexiune la rețea sau o imagine de înaltă rezoluție.

Exemplu simplu fără proxy

class RealImage {
    private String filename;
 
    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }
 
    private void loadFromDisk() {
        System.out.println("Loading " + filename);
    }
 
    public void display() {
        System.out.println("Displaying " + filename);
    }
}
 
public class Main {
    public static void main(String[] args) {
        RealImage img1 = new RealImage("photo1.jpg");
        img1.display();
 
        RealImage img2 = new RealImage("photo1.jpg"); // se reîncarcă imaginea!
        img2.display();
    }
}

Probleme:

Structura pattern-ului

Structura pattern-ului Proxy implică 4 elemente principale:

Exemplu complet

1. Interfața Subject

interface Image {
    void display();
}

2. RealSubject

class RealImage implements Image {
    private String filename;
 
    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }
 
    private void loadFromDisk() {
        System.out.println("Loading " + filename);
    }
 
    @Override
    public void display() {
        System.out.println("Displaying " + filename);
    }
}

3. Proxy

class ProxyImage implements Image {
    private RealImage realImage;
    private String filename;
 
    public ProxyImage(String filename) {
        this.filename = filename;
    }
 
    @Override
    public void display() {
        if (realImage == null) { // lazy loading
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}

4. Clientul

public class Main {
    public static void main(String[] args) {
        Image image1 = new ProxyImage("photo1.jpg");
        Image image2 = new ProxyImage("photo2.jpg");
 
        // Prima afișare => încarcă obiectul real
        image1.display();
        // A doua afișare => folosește obiectul deja creat
        image1.display();
 
        image2.display();
    }
}

Avantaje

Dezavantaje

Tipuri comune de Proxy

  1. Virtual Proxy – crează obiectul real doar la prima utilizare (lazy loading).
  2. Remote Proxy – intermediar pentru obiecte aflate pe alte calculatoare/servere.
  3. Protection Proxy – controlează accesul la obiecte sensibile.
  4. Caching Proxy – memorează rezultatele pentru a evita recalcularea.

Exerciții

Task 1 (8p)

Dorim să prelucrăm forme geometrice, pe care să le afișăm în diverse formate: text și JSON https://datatracker.ietf.org/doc/html/rfc8259. Pentru un design decuplat între formele prelucrate și tipurile de formate dorite, implementați conversia folosind patternul Visitor.

Problema de pe DevMind va avea două task-uri, corespunzătoare celor două tipuri de Visitor. Pentru simplitatea implementării acestor Visitors, vă sugerăm să urmăriți TODO-urile din schelet.

Exemplu de format text

   Circle - radius = 30

Exemplu de format JSON

   {
      "Circle": {
         "radius": 30
      }
   }

Task 2 (2p)

Într-un joc futurist, creaturile sunt generate procedural (automat), iar pentru a evita haosul, jocul folosește un singur „Generator Universal de Creaturi”.

Acest generator trebuie să creeze mai multe tipuri de creaturi:

În joc trebuie să existe o singură instanță a generatorului (Singleton), altfel apar buguri în simulare.

Generatorul trebuie să includă un Factory Method pentru crearea creaturilor pe baza tipului lor.

Urmăriți scheletul pentru a putea implementa generatorul universal de creaturi.

Resurse și link-uri utile