Laborator 2

I. Principiul Open-Closed

- pornim de la urmatorul exemplu:

      class Line{
          int x1,y1,x2,y2;
          ...
          void show(){
              System.out.println("("+x1+","+y1+")-("+x2+","+y2+")");
              }
          }
      ...
      Line l=new Line(10,100,20,50);
      l.show();

- in clasa Line avem o metoda care ne afiseaza linia sub forma a doua puncte
- dorim sa avem posibilitatea de a afisa linia si sub forma a doua puncte insotite de lungimea liniei
- daca dorim sa nu schimbam numele metodei de afisare, avem trei posibilitati:

a) rescriem metoda show() pentru noul format de afisare dorit:
          void show(){
              System.out.println("("+x1+","+y1+")-("+x2+","+y2+"),"+length());     
              }
- aceasta metoda de modificare a codului este cea mai proasta deoarece pot sa existe clase sau chiar alte programe care folosesc clasa Line si se bazeaza pe vechiul format de afisare. In momentul in care am modificat formatul de afisare este posibil ca toate aceste intrebuintari mai vechi ale metodei show() sa nu mai corespunda scopului pentru care au fost utilizate.
b) modificarea metodei show() pentru a lua in considerare si noul format dorit:
          class Line{
              ...
              boolean afisVechi=true;
              ...
              void show(){
                  if(afisVechi){
                      System.out.println("("+x1+","+y1+")-("+x2+","+y2+")");
                  }else{
                      System.out.println("("+x1+","+y1+")-("+x2+","+y2+"),"+length());
                      }
              }
          } 
          ...
          Line l=new Line(10,100,20,50);
          l.show();
          l.afisVechi=false;
          l.show();
          l.afisVechi=true;
- in aceasta situatie s-a adaugat un flag, afisVechi, in clasa Line si formatul afisarii depinde de valoarea acestui flag. De fiecare data cand dorim sa afisam conform noului format, prima oara setam corespunzator flagul, afisam si refacem valoarea implicita a flagului.
Dezavantaje:
  1. daca uitam sa refacem flagul, toate afisarile ulterioare ale liniei respective vor avea un format gresit
  2. uneori vom dori sa afisam conform noului format si tot cu noul format setat sa pasam linia unei alte metode, care este posibil sa o afiseze, tot in noul format. Daca cea de-a doua metoda, dupa ce a afisat linia reseteaza flagul la vechiul format, conform procedurii standard, la revenirea in prima metoda, linia, in loc sa aiba setat noul format, cum ne-am astepta, il va avea setat pe cel vechi si afisarile ulterioare pot sa fie incorecte.
  3. atributul adaugat clasei Line ocupa un spatiu de memorie care poate deveni semnificativ pentru un numar foarte mare de linii sau in conditiile unor sisteme embedded.
c) cream o clasa auxiliara, care mosteneste Line si care suprascrie metoda show():
         class AuxLine extends Line{
             ...
             public AuxLine(Line l){
                 super(l.x1,l.y1,l.x2,l.y2);
                 }

             void show(){
                 System.out.println("("+x1+","+y1+")-("+x2+","+y2+"),"+length());
                 }
         }
         ...
         Line l=new Line(10,100,20,50);
         l.show();
         AuxLine al=new AuxLine(l);
         al.show;
- clasa AuxLine are si un nou constructor pentru a fi mai usor sa o cream pornind de la o linie data.
- cand evem nevoie sa afisam in formatul nou, cream o instanta de AuxLine din linia pe care dorim sa o afisam si ii apelam metoda show().
- in acest fel nu apare niciun fel de modificare la nivelul clasei Line si nici nu este posibil ca prin intermediul lui AuxLine sa modificam setarea originara.

Din exemplele de mai sus constatam ca atat in designul claselor cat si ca stil de programare trebuie sa tinem cont de doua aspecte importante:
a) odata ce am folosit o clasa, nu mai avem voie sa ii aducem modificari de natura sa ii modifice functionalitatea existenta. Singurele modificari care avem voie sa le facem sunt cele care adauga noi functionalitati, pastrandu-le neschimbate pe cele vechi.
b) cand proiectam o clasa si metodele ei, trebuie sa avem in vedere ca este foarte probabil ca pe viitor sa dorim sa ii adaugam noi functionalitati si de aceea conceperea clasei trebuie realizata de o maniera care sa permita adaugarea usoara a acestor noi facilitati.

Principiul Open-Closed sistematizeaza observatiile de mai sus sub forma: o clasa trebuie sa fie inchisa pentru modificari, dar deschisa pentru adaugiri.

II. Dependency Inversion Principle

Consideram ca avem de implementat o baza de date care sa ne permita sa stocam intr-un mod unitar siruri de caractere atat in memorie cat si pe disc.

- datele stocate in memorie nu trebuie sa fie salvate in mod permanent, pe cand cele de pe disc, da.
- in Java stim ca este nevoie, dupa ce am lucrat cu un fisier, sa folosim metoda close(), pentru a inchide fisierul in care am lucrat, asigurandu-ne ca datele au fost efectiv scrise in el. Dupa o prima evaluare a problemei ajungem la urmatoarea ierarhie de clase:

        interface DataBase{
               void add(String s);
               void save();
               }

         class MemoryDataBase implements DataBase{
               Vector<String> strings= new Vector<String>();
               void add(String s){
                     strings.add(s);
                     }
               void save(){}
               } 

         class DiskDataBase implements DataBase{
               PrintWriter file=null;
               public DiskDataBase(String fileName) throws IOException{
                     file=new PrintWriter(new FileWriter(fileName));
                     }

               void add(String s) throws IOException{
                     file.println(s);
                     }

               void save() throws IOException{
                     file.close();
                     }
               }

- metoda save() a fost adaugata interfetei DataBase pentru ca daca avem o variabila care implementeaza DataBase trebuie sa putem salva in siguranta pe disc sirurile din baza de date.
- la o privire mai atenta, insa, ne dam seama ca save() nu isi are rostul decat in DiskDataBase si ca in MemoryDataBase ea nu este folosita. Mai mult decat atat, nu corespunde conceptual niciunei realitati fizice.
- ar mai putea exista si alte situatii in care, de exemplu, sa avem o baza de date distribuita intr-o retea si care sa aiba nevoie de niste metode proprii pentru a putea fi implementata.
- daca am urma modelul de gandire de pana acum am include si aceste noi metode care ii sunt necesare bazei de date distribuite tot in interfata DataBase, ceea ce ar duce la urmatoarele probleme:

  1. toate clasele care implementeaza DataBase vor trebui sa implementeze si acele metode noi, desi acestea nu au nicio semnificatie in cadrul acelei clase.
  2. tinand cont ca vom avea tendinta sa adaugam noi metode in DataBase de fiecare data cand mai apare un caz particular de baza de date, tot codul care a fost scris pana atunci trebuie completat cu noile implementari de metode. Acestea pot deveni o problema daca deja codul a fost livrat la un client si noi nu mai avem acces la el.
  3. cand cineva va folosi interfata DataBase va avea mai multe metode in ea si nu va sti care dintre ele si in ce context trebuie folosite.
Din observatiile de mai sus reies aspectele fundamentale care stau la baza principiului Dependency Inversion:
a) o clasa de baza nu are voie sa declare sau sa implementeze operatii care sunt necesare doar unor derivate ale sale (ea trebuie implementata ca si cum nu am sti ce clase vom deriva din ea)
b) modulele la nivelul carora trebuie sa existe implementari concrete, detaliate, trebuie sa fie izolate de restul programului si accesate doar prin intermediul unor abstractizari care sa izoleze pe cel care le utilizeaza de posibilele modificari de detaliu ce pot apare la nivel de implementare
Pentru a respecta cerintele DIP exemplul de mai sus poate fi rescris in doua feluri:
  1. daca adaugirile de siruri sunt rare, putem renunta cu totul la metoda save() si in clasa DiskDataBase, metoda add(), la fiecare apel al ei sa deschida fisierul, sa scrie sirul si sa inchida fisierul. Aceasta implementare poate fi folosita doar in cazurile in care accesarile bazei de date sunt rare fiindca altfel faptul ca la fiecare accesare avem nevoie de o deschidere si inchidere de fisier (operatii care necesita timp mare de executie), ar face aceasta implementare neutilizabila.
  2. putem adauga un layer suplimentar in ierarhia de clase, layer menit sa abstractizeze toate operatiile necesare pentru un anumit tip de baze de date:

- interfata AbstractDiskDatabase este folosita ca layer de abstractizare pentru toate tipurile de baze de date care necesita metoda save(). In acest fel am izolat metoda save() de interfata DataBase. La obiectele care implementeaza DataBase nu vom mai sti in mod concret daca au nevoie sau nu de save(). In acest caz putem folosi metoda testarii tipului cu operatorul instanceof, sau putem apela la implementari mai complexe, folosind design patterns care vor fi detaliate ulterior:

        void lastAddName(DataBase db, String name){
          db.add(name);
                if(db.instanceof AbstractDiskDatabase){
                      db.save();   
                      }
          }       

- functia lastAddName scrie ultimul string al aplicatiei in orice fel de baza de date, apeland metoda save()daca este necesar.
III. Probleme:

1. Pornind de la clasele:

    abstract class Shape{
        abstract double perimeter(); 
        }

  class Point{
        int x,y;
        Point(int x,int y){
              this.x=x;
              this.y=y;
              }
        }

Se cere:
- sa se implementeze o ierarhie din urmatoarele shape-uri si programul de test aferent: Line, Angle, Circle, Square, Triangle
- un Angle e definit prin unghi si lungimea laturilor (aceeasi)
- Line si Triangle sunt definite prin coordonatele punctelor constituente
- Circle e definit prin lungimea razei
- Square e definit prin lungimea laturii
- shape-urile care au coordonate de puncte vor stoca aceste puncte intr-un array
- shape-urile care au nevoie de o lungime trebuie sa implementeze o interfata speciala care defineste metoda double getLength()
- shape-urile care determina o suprafata trebuie sa implementeze o interfata speciala care defineste metoda double getArea()
- se va urmari sa nu existe niciun cod duplicat si nici abstractizari care sa nu corespunda aspectelor implementate. Pentru aceasta se poate interveni inclusiv la nivelul claselor date, daca este necesar.

2. Pornind de la clasa si interfata:

    abstract class CelestialBody{
       abstract double speed();
       }

    interface Orbital{
       double revolutionPeriod();
       double radius();
       }

Se cere:
- sa se implementeze o ierarhie din urmatoarele clase de corpuri ceresti si programul de test aferent: Star, Planet, Comet, Moon
- Planet si Moon au orbite si implementeaza interfata Orbital. In cazul lor sunt date perioada de revolutie in zile si raza orbitei in km
- pentru Comets viteza este implementata sub forma unei constante
- viteza va fi data si ceruta in m/s
- orbitele sunt aproximate prin cercuri si vitezele intotdeauna sunt constante
- Star este considerata ca fiind imobila
- oricare dintre corpurile ceresti care au viteza implementeaza o interfata speciala care defineste metoda boolean clearPath(int years) care returneaza true daca pentru perioada de timp data nu sunt prevazute coliziuni. Pentru necesitatile programului, coliziunile sunt date ca o functie de forma: ct*years>0.5 , unde ct este o constanta reala specifica corpului ceresc
- se va urmari sa nu existe niciun cod duplicat si nici abstractizari care sa nu corespunda aspectelor implementate. Pentru aceasta se poate interveni inclusiv la nivelul codului dat, daca este necesar.\\