Laborator 5

I. FACTORY & FACTORY METHOD

Consideram o clasa Drawing care contine desene compuse din instante a trei subclase a clasei abstracte Shape. In acelasi timp, clasa Drawing mentine si pentru fiecare tip de Shape numarul instantelor sale. Intr-o prima varianta putem implementa cerintele in felul urmator:

     ...
     class Main{
        public static void main(String[] args){
            Drawing d=new Drawing();
            Circle c=new Circle(30,50,10);
            d.shapes.add(c);
            ++d.nCircles;
            Rectangle r=new Rectangle(0,0,10,30);
            d.shapes.add(r);
            ++d.nRectangles;
            ...
            }
        ...
        }

- remarcam ca in aceasta implementare, de fiecare data cand cream un nou Shape trebuie sa il adaugam in lista de Shape-uri a Drawing-ului si sa incrementam contorul corespunzator. In cazul in care exista mai multe locuri in program in care trebuie sa cream Shape-uri, rezulta ca in fiecare din aceste locuri trebuie sa duplicam tot codul respectiv. Mai mult decat atat, daca de exemplu mai trebuie sa introducem inca o operatie necesara la crearea Shape-urilor (de exemplu sa punem operatia intr-o lista de undo) trebuie sa modificam in toate locurile din program in care sunt create Shape-uri, ceea ce poate duce la omisiuni sau la alte bug-uri. O alta problema la aceasta implementare este ca trebuie sa tinem minte si sa implementam corect toata secventa de operatii necesara crearii unui Shape, ceea ce in cazul programelor mai mari, cu multe clase si interactiuni complexe poate sa duca la multe bug-uri greu de depistat.
- tinand cont de observatiile de mai sus, rezulta ca ar fi mult mai bine sa avem toata secventa de operatii necesara crearii unui Shape incapsulata intr-o metoda a clasei Drawing, metoda care sa se ocupe de tot managementul crearii acelui obiect. Observam ca anumite aspecte la crearea Shape-ului nu pot fi implementate in constructorul acestuia fiindca ele nu tin de Shape in sine, ci de contextul in care acesta e folosit.
- ajungem astfel la urmatoarea implementare:

    class Drawing{
         ...
         Circle createCircle(int xc, int yc, int r){
            Circle c=new Circle(xc,yc,r);
            shapes.add(c);
            ++nCircles;
            return c;
            }

            ...
         Rectangle createRectangle(int x1, int y1, int x2, int y2){
            Rectangle r=new Rectangle(x1,y1,x2,y2);
            shapes.add(r);
            ++nRectangles;
            return r;
            }
            ...
         }
                       ...

     class Main{
        public static void main(String[] args){
            Drawing d=new Drawing();
            d.createCircle(30,50,10);
            d.createRectangle(0,0,10,30);
            ...
            }
        ...
        }

- aceasta implementare elimina inconvenientele anterioare, aducandu-ne urmatoarele avantaje:

a) crearea unui obiect se face mult mai simplu, intr-o singura linie de cod, fara a mai fi nevoie sa tinem minte managementul necesar acestuia;
b) daca pe viitor avem de adaugat/modificat operatii la crearea Shape-urilor, acestea se vor face intr-un singur loc;

- aceasta implementare elimina inconvenientele anterioare, aducandu-ne urmatoarele avantaje:

a) crearea unui obiect se face mult mai simplu, intr-o singura linie de cod, fara a mai fi nevoie sa tinem minte managementul necesar acestuia;
b) daca pe viitor avem de adaugat/modificat operatii la crearea Shape-urilor, acestea se vor face intr-un singur loc;

- in aceasta implementare clasa Drawing devine un Factory pentru Shape-urile sale, iar metodele createCircle, createRectangle, etc, devin Factory Methods.
- in functie de cerinte, acest pattern poate deveni mai complex, uzand de exemplu de o clasa separata (Shapes Factory) care sa se ocupe doar cu crearea obiectelor, sau implementand o metoda create generica, care pe baza unui selector sa creeze orice fel de shape necesar.

    II. ABSTRACT FACTORY

- consideram ca avem de implementat un joc cu doua niveluri distincte, fiecare nivel implicand o calatorie, in primul dintre ele pe Pamant, iar in cel de-al doilea in spatiul cosmic. In fiecare caz ne vom intalni pe parcursul calatoriei cu doua aspecte cu care vom interactiona: obiecte si fiinte. In program, dupa ce jucatorul selecteaza nivelul dorit, vor incepe sa-i apara diverse instante de obiecte si fiinte cu care va interactiona. Proportional, vor apare de doua ori mai multe obiecte decat fiinte.
- intr-o prima implementare, putem gandi structura in felul urmator:

     abstract class Aspect{...}

     class EarthAspect extends Aspect{...}

     class EarthObject extends EarthAspect{...}

     class EarthBeing extends EarthAspect{...}

     class ExtraterestrialAspect extends Aspect{...}

     class ExtraterestrialObject extends ExtraterestrialAspect{...}

     class ExtraterestrialBeing extends ExtraterestrialAspect{...}

     class Factory{
        EarthObject createEarthObject(){...}
        EarthBeing createEarthBeing(){...}
        ExtraterestrialObject createExtraterestrialObject(){...}
        ExtraterestrialBeing createExtraterestrialBeing(){...}
        ...
        }

     class Game{
        Enum Level{Earth,Space};
        Level level=Level.Earth;
        Factory f=new Factory();
        ...
        Aspect createAspect(){
            switch(level){
                case Earth:
                    if(Math.random()<=0.33)return f.createEarthBeing();
                    return f.createEarthObject();
                    break;
                case Space:
                    if(Math.random()<=0.33)return f.createExtraterestrialBeing();
                    return f.createExtraterestrialObject();
                    break;
                }
            ...
            }
        ...
        }

- in aceasta implementare avem un singur Factory care ne permite oricand sa cream toate aspectele din joc. Aceasta implementare are ca dezavantaje faptul ca in orice punct din program in care vrem sa cream un nou aspect trebuie preliminar sa verificam nivelul curent. Daca nu facem aceasta putem din greseala sa mixam aspectele terestre cu cele cosmice. Totodata, daca introducem un nou nivel in joc (ex: vis) in toate locurile in care am creat diverse aspecte va trebui sa modificam codul, ceea ce iarasi poate duce la bug-uri.
- observand faptul ca exista o simetrie intre diverse niveluri putem sa cream un Factory generic care sa aiba implementari specifice fiecarui nivel:

        abstract class Factory{
        abstract Aspect createObject();
        abstract Aspect createBeing();
        Aspect create(){
            if(Math.random()<=0.33)return createBeing();
            createObject();
            }
        }

            class EarthFactory extends Factory{...}

      class SpaceFactory extends Factory{...}

      class Game{
        ...
        Factory f=new EarthFactory();
        ...
        Aspect createAspect(){return f.create();}
        ...
        }

- in aceasta implementare fiecare nivel are un Factory propriu, care deriva din Factory abstract si care trebuie sa implementeze doua Factory Methods, pentru crearea obiectelor si fiintelor specifice acelui nivel
- la nivelul clasei Factory avem si o metoda create care deja poate fi implementata la acest nivel, care creaza diversele aspecte specifice unui nivel in proportia ceruta de specificatii. Astfel nu mai se centralizeaza codul pentru crearea diverselor aspecte. In aceasta metoda nu avem nevoie de un selector de nivel, deoarece intotdeauna avand instantiat doar un Factory specific nivelului curent, se vor apela metodele din acesta
- aceasta implementare elimina dezavantajele de la implementarea anterioara si in plus face posibila o implementare mai usoara a noilor nivele, printr-o decuplare mai mare a claselor implicate, rezultata ca urmare a abstractizarii codului creational

    III. PROTOTYPE si clone()

Consideram ca avem un compilator de expresii numerice care accepta numere, operatori unari si binari si apeluri de functii. Odata expresia compilata, ea este reprezentata intern sub forma unui arbore, ca in exemplul urmator. Pentru expresia (a+1)*(10-2)+F(a,b)

- dorim sa pastram atat expresia originara care a rezultat in urma compilarii, cat si o expresie optimizata care sa necesite un timp mai mic de evaluare. Expresia optimizata se obtine prin transformari matematice operate pe expresia originara, de exemplu (10-2) poate fi optimizat din start ca fiind 8 si atunci se pot inlocui in arbore nodurile corespunzator scaderii si operanzilor sai cu un nod corespunzator constantei 8. Pentru aceasta trebuie sa avem o modalitate de a realiza operatia Deep Copy pe arborele dat, pentru a obtine o copie completa a acestuia, copie pe care o vom optimiza. Toate nodurile arborelui sunt subclase ale clasei abstacte Node
- o prima modalitate de abordare a problemei de a duplica acest arbore este sa realizam o functie recurenta de traversare, care pentru fiecare nod sa ii determine tipul si in functie de acesta sa realizeze duplicarea specifica acelui nod. Aceasta abordare are mai multe dezavantaje, deoarece cupleaza foarte strans ierarhia de noduri, cu toata logica aferenta crearii nodurilor de functia de duplicare. In acelasi timp, daca introducem un tip nou de nod sau daca modificam unul existent, trebuie sa modificam in acelasi timp si functia de duplicare
- tinand cont de cele de mai sus vom urmari sa imlementam la nivelul fiecarui tip de nod o metoda prin care acel nod sa se autoduplice (cloneze) impreuna cu toti "copiii" sai. Avand aceasta metoda de clonare la dispozitie, realizam mai multe avantaje cum sunt:

            a) nu mai trebuie sa realizam apel recursiv, fiindca odata ce am apelat clonarea radacinii arborelui, ea la randul sau va clona intern toti copiii radacinii si tot asa mai departe
b) nu mai avem nevoie de selector extern care sa realizeze diferite actiuni in functie de tipul nodului curent, si mai mult decat atat, nu mai avem deloc nevoie sa implementam logicile creationale specifice diverselor tipuri de noduri in afara claselor lor, realizand decuplarea metodei de duplicare de ierarhia de noduri
c) daca adaugam o clasa sau modificam un nod in ierarhie, atunci doar prin scrierea metodei clone() pentru noul nod sau modificarea ei pentru nodul modificat, este suficienta pentru a putea realiza clonarea
- in Java exista o metoda standard clone() chiar de la nivelul lui Object, care realizeaza un Shallow Copy, adica duplica doar instanta curenta, nu si referintele ei (referintele noii instante vor pointa tot catre obiectele apartinand referintelor vechii clase). Totodata clone() nu poate fi apelata pentru o clasa data decat daca acea clasa implementeaza explicit interfata Clonable, altfel se genereaza o exceptie. Problema cu clone() in Java este ca in principiu nu se poate accesa pentru o clasa abstracta. Din acest motiv, tinand cont ca Node este o clasa abstracta, vom folosi o Factory Method pentru duplicare: abstact class Node{ abstact Node dup(); } class Ct extends Node{ double c =0.0; Ct(double c){ this.c=c; } Node dup(){return new Ct(c);} } class Var extends Node{ String name=""; Var(String name){ this.name=name; } Node dup(){return new Var(name);} } class BinOp extends Node{ Enum Op{Add,Mul,Div,Mod}; Op op; Node left,right; BinOp(Op op,Node left, Node right){ this.op=op; this.left=left; this.right=right; } Node dup(){return new BinOp(op,left.dup(),right.dup());} } class Call extends Node{ String fName; Vector<Node> args=new Vector<Node>(); Call(String fName,Vector<Node> args){ this.fName=fName; this.args=args; } Node dup(){ Vector<Node> v=new Vector<Node>(); for(Node n:args) v.add(n.dup()); return new Call(fName,v); } ... } ...

- uneori chiar si in cazul in care nu avem ierarhii de clase este bine sa folosim crearea unui nou obiect pornind de la unul deja existent, obiectul existent devenind astfel prototipul obiectului nou creat. Aceasta operatie este, printre altele foarte utila cand avem de propagat mai departe la noile obiecte informatia care deja a fost inmagazinata in prototip. De exemplu daca avem o clasa care modeleaza o retea neuronala, care deja a invatat pe anumite date de intrare, este bine ca in loc sa folosim constructori pentru a creea noi retele neuronale, care sa porneasca de la 0, sa folosim vechea retea neuronala ca un prototip pentru urmatoarele instante ale clasei respective.

    IV. SINGLETON

- uneori este nevoie ca accesul la o resursa comuna, cum ar fi memoria, un fisier, un socket de retea, etc sa se faca in mod centralizat, printr-o singura instanta a clasei care se ocupa de managementul acelei resurse. De exemplu, daca implementam o baza de date intr-un fisier, trebuie sa ne asiguram ca nu vor fi deschise in program mai multe handlere la acel fisier. Totodata mai sunt situatii in care in program trebuie sa existe doar o singura instanta a unei clase, de exemplu mediul sau tabla de joc pentru un joc sau un simulator. In aceste situatii se foloseste pattern-ul SINGLETON, care garanteaza ca vom folosi intotdeauna doar o instanta a unei clase:

       public class GameBoard{
        private GameBoard(){...}
        static private GameBoard _instance=null;
        static public GameBoard instance(){
            if(_instance==null)_instance=new GameBoard();
            return _instance;
            }
        }
               ...
        GameBoard gb=GameBoard.instance();

- clasa GameBoard are unicul constructor declarat protected pentru a fi imposibil sa se creeze instante ale acestei clase cu "new GameBoard" din afara clasei
- metoda statica instance(), atunci cand este apelata, verifica daca este deja creata o instanta a clasei GameBoard si daca nu este, o creaza. Aceasta instanta va fi unica creata si intotdeauna va fi returnata spre folosinta. Astfel ne asiguram ca este respectata cerinta de a se folosi mereu doar o instanta a clasei, care in aceasta situatie se numeste clasa singleton

    V. Tema

Sa se scrie clasele cu implementarile lor, diagrama UML si un scurt program de test (Main) pentru urmatoarele cerinte:
- se considera un magazin cu produse de sezon pentru vara si iarna
- in magazin nu pot exista la un moment dat decat produse corespunzatoare unui singur sezon
- cand se schimba sezonul, inventarul se goleste
- pentru fiecare sezon exista o lista de produse, fiecare avand o anumita cantitate in inventar, la un moment dat - cand se cere prima oara un nou produs (care trebuie sa fie corespunzator sezonului), sau cand se epuizeaza produsul de pe stoc, magazinul face comanda pentru a aduce un nou stoc din acel produs
- trebuie implementate, printre alte metode considerate necesare, urmatoarele metode corespunzatoare actiunilor:
- listare inventar existent (produse si cantitati)
- adaugare nume produs in lista celor de sezon
- cerere reinoire stoc pentru un produs dat
- setare sezon