Lab 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 functionalitatea unui manager de magazin. Acesta da de lucru angajatilor (vanzatorii din magazin), cu ajutorul unei metode doSomeWork(Salesman s), care apeleaza o metoda sale, implementata in Salesman. Clasele arata in felul urmator:

class Salesman{

    void sale();
    }

class Manager{

    void doSomeWork(Salesman s){
        s.sale();
        }
    }

Desi aceasta abordare este simpla, ea are unele probleme. De exemplu, daca la angajati dorim sa adaugam si un sofer (Driver), va trebui sa rescriem toate metodele din Manager care depind de Salesman, pentru a include si noul tip de angajat. Totodata, soferul nu are o metoda "sale" ci ar trebui sa aiba una "drive". Greseala facuta este ca Manager depinde de o clasa particulara, in loc sa depinda de o interfata generica. Pentru a corecta aceasta greseala, clasele se pot implementa astfel:

interface Worker{

    void work();
    }

class Salesman implements Worker{

    void work(){...}
    }

class Driver implements Worker{

    void work(){...}
    }

class Manager{

    void doSomeWork(Worker w){
        w.work();
        }
    }

Pentru aceasta versiune, diagrama de clase arata astfel:

In acest caz am folosit interfata Worker pentru a abstractiza functionalitatea necesara oricarui tip de angajat. Worker nu are metode specifice, gen "sale" sau "drive", ci o metoda generica, "work". In acelasi timp, am eliminat din clasa Manager orice referinte la un anumit tip specific de angajat si am folosit doar interfata generica Worker. S-a reusit astfel decuplarea clasei Manager de un anumit tip de angajat si a devenit posibil sa putem oricand sa adaugam noi tipuri de angajati, fara sa trebuiasca sa modificam si clasa Manager.

        Din observatiile de mai sus reies aspectele fundamentale care stau la baza principiului Dependency Inversion:
        a) modulele de nivel inalt nu trebuie sa depinda de modulele de nivel coborat. Ambele trebuie sa depinda doar de abstractiuni.
        b) abstractiunile nu trebuie sa depinda de detalii. Detaliile trebuie sa depinda si ele doar de abstractiuni.

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 (doar Main - nu e necesar JUnit): 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.