Lab 5

I. FACTORY

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{
         ...
         public Circle createCircle(int xc, int yc, int r){
            Circle c=new Circle(xc,yc,r);
            shapes.add(c);
            ++nCircles;
            return c;
         }

         ...
         public 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;

- in aceasta implementare clasa Drawing devine un Factory pentru Shape-urile sale

- 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. FACTORY METHOD

Consideram o aplicatie de gestiune integrata a mai multor utilitati, cum ar fi Apa, Curent electric, etc. Cand un client vine la ghiseu, dintr-un meniu se selecteaza fereastra specifica de gestiune (de exemplu pentru Apa) si din alt meniu se va selecta operatia dorita: adaugare/modificare/stergere/facturare abonat. Clientii pentru diferite utilitati pot avea optiuni diferite, gen tipul retelei electrice (pentru Curent) sau diametrul racordului de apa (pentru Apa), dar la toti clientii operatiile vor fi cele 4 specificate. Putem astfel sa avem un Client abstract din care sa derivam Clientii specifici.

Problema care apare este: cand selectam in meniul aplicatiei o anumita operatie pentru tipul de gestiune selectat (de exemplu creere client la reteaua de apa), de unde stim sa cream clientul specific corespunzator?

Factory Method ne ajuta in acest sens oferind urmatoarea solutie: in clasa abstracta din care mostenim toate tipurile de gestiuni, declaram o metoda abstracta care va avea rol de creere a clientilor specifici. In clasele care implementeaza gestiunile specifice, vom realiza implementarea specifica a acestei metode ( Factory Method ) pentru a creea clientii specifici de care avem nevoie. Astfel, avem urmatoarea ierarhie de clase, cu codul asociat:

    ...
    abstract class Client{
        public abstract void modificare();
        public abstract void facturare();
        ...
    }

    class ClientApa extends Client{...}

    class ClientCurent extends Client{...}

    abstract class Gestiune{
        public abstract Client creazaClient();
        ...
    }

    class class GestiuneApa extends Gestiune{
        public Client creazaClient() {return new ClientApa();}
    }

    class class GestiuneCurent extends Gestiune{
        public Client creazaClient(){return new ClientCurent();}
    }

    class Aplicatie{
        private Gestiune gestiune;
        private Client client;

        void preluareActiuneMeniu(){
            if(/*selectare gestiune apa*/){
                gestiune=new GestiuneApa();
            } else if(/*selectare adaugare client*/){
                client=gestiune.creazaClient();
            }
            ...
        }
        ...
    }

Se realizeaza astfel tratarea generica si implicit decuplarea atat dintre clasa aplicatie si clasele de gestiune, cat si dintre aplicatie si tipurile de clienti. Astfel se vor putea adauga foarte usor noi tipuri de gestiune, cu clientii asociati lor, modificandu-se doar intrun singur lor clasa Aplicatie, si anume in metoda preluareActiuneMeniu.

In design pattern-urile creationale care opereaza pe ierarhii de clase nu este indicat sa se foloseasca metode factory care sa primeasca parametrii specifici crearii anumitor tipuri de obiecte. Aceasta cupleaza foarte strans clasele intre ele si practic elimina avantajul generalitatii codului care s-a obtinut anterior prin abstractizarea ierarhiei, fiindca oricand putem avea nevoie pentru un nou tip de obiect de un alt parametru pe care nu l-am prevazut anterior, si pentru asta va trebui sa modificam peste tot in clasele derivate si in folosirea lor. In clasele de tip factory este indicat sa se foloseasca cel mult un parametru, care sa indice tipul obiectului ce va fi creat. Obiectul creat va fi initializat in interiorul metodei factory cu valori implicite, urmand ca ulterior acest obiect sa fie setat la valorile dorite folosind metodele lui proprii de tip "set...". Uneori se poate folosi ca argument pentru metodele creationale o clasa abstracta (Argument) care este derivata corespunzator fiecarei clase obiect ce trebuie create, de exemplu: ArgumentApa, ArgumentCurent. In acest caz metodele factory vor deveni: "Client creazaClient(Argument a)", urmand sa le fie pasat obiectul de tip Argument necesar initializarii lor.

In exemplul de mai sus, logica aplicatiei ne permite sa impartim aplicatia in doua niveluri:

1. nivelul aplicatiei, in care dispunem doar de clienti si de metode de gestiune generice. La acest nivel, vom putea folosi doar metode generice, de exemplu o metoda "save" de care trebuie sa dispuna toti clientii, pentru a putea sa fie salvati in baza de date.

2. nivelul specific unei gestiuni, in interiorul unei clase concrete de tip "Gestiune...". Aici stim exact tipul gestiunii concrete in care ne aflam si totodata stim ca nu putem avea decat un tip de client care este specific acelei gestiuni (de exemplu in GestiuneApa nu se opereaza decat cu ClientApa). La acest nivel putem aplica metode specifice gestiunii si clientului concret, gen "setDiametruRacord(double diametru)"

Tinand cont de aceste niveluri, initializarea clientilor si ulterior folosirea lor trebuie sa foloseasca la nivelul aplicatiei doar metode generice (definite abstract in clasele Client si Gestiune), iar orice operatii specifice se vor desfasura doar in punctele din program (in interiorul claselor "Gestiune..." si "Client..."), unde suntem siguri de tipul instantei lor particulare si unde nu trebuie sa folosim if-uri in cascada cu teste de tipul "instanceof", pentru a putea afla tipul concret.

III. 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{
        public EarthObject createEarthObject(){...}
        public EarthBeing createEarthBeing(){...}
        public ExtraterestrialObject createExtraterestrialObject(){...}
        public ExtraterestrialBeing createExtraterestrialBeing(){...}
        ...
     }

     class Game{
        public Enum Level{Earth,Space};
        private Level level=Level.Earth;
        private Factory f=new Factory();
        ...
        public 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{
        public abstract Aspect createObject();
        public abstract Aspect createBeing();
        public Aspect create(){
            if(Math.random()<=0.33)return createBeing();
            createObject();
            }
        }

      class EarthFactory extends Factory{...}

      class SpaceFactory extends Factory{...}

      class Game{
        ...
        private Factory f=new EarthFactory();
        ...
        public 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

IV. 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{
         public abstact Node dup();    
      }

      class Ct extends Node{
         private double c =0.0;
         public Ct(double c){
            this.c=c;
         }
         Node dup(){return new Ct(c);}
      }

      class Var extends Node{
         private String name="";
         public Var(String name){
            this.name=name;
         }  
         Node dup(){return new Var(name);}
      }

      class BinOp extends Node{
         public Enum Op{Add,Mul,Div,Mod};
         private Op op;
         private Node left,right;
         public BinOp(Op op, Node left, Node right){
            this.op=op;
            this.left=left;
            this.right=right;
         }
         public Node dup(){return new BinOp(op,left.dup(),right.dup());}
      }

      class Call extends Node{
         private String fName;
         private Vector<Node> args=new Vector<Node>();
         public Call(String fName,Vector<Node> args){
            this.fName=fName;
            this.args=args;
         }
         public Node dup(){
            private 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.

V. 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

VI. Tema

DE IMPLEMENTAT (inainte de a citi)

Sa se scrie clasele cu implementarile lor, diagrama UML si un scurt program de test (Main) pentru urmatoarele cerinte: - se considera un magazin (unic) 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

DE CITIT

Head First - Design Patterns Factory: pp 109-176 Prototype: pp 626-627 Singleton: pp 177

Addison Wesley - GOF, Elements Of Reusable Object Oriented Software Factory: pp 125-132, pp 211-218 Prototype: pp 288-296 Singleton: pp 308-313

Nota:

1. Vom discuta la laborator exemplele din bibliografia indicata si veti primi la teste (cand le vom stabili) intrebari de la aceste capitole

2. Este recomandata lecturarea pana la laboratorul urmator