În Tips & Tricks

Programarea orientată pe obiect și dezvoltarea de aplicații – 5 principii de design

programare-orientata-obiect

Programarea orientată pe obiect porneşte de la o serie de principii de bază precum: 1. posibilitatea de reutilizare a codului, 2. capacitatea de a-l extinde cu uşurință, 3. încapsularea proprietăților şi comportamentului obiectelor, 4. mentenabilitate. Totuşi, aceste lucruri nu vin de la sine iar un software developer bun trebuie să urmărească o serie de reguli şi principii pentru a se asigura că programul său respectă criteriile de mai sus.

Principiile de proiectare și programare orientată pe obiect s-au dezvoltat în timp, pe baza problemelor practice şi a experienței dobândite de către alți programatori. Aceste principii au fost formulate şi standardizate, în cele din urmă rezultând o serie destul de lungă de principii de design.

Dintre acestea, cele mai aplicate şi cunoscute sunt cele cinci care au fost rezumate sub acronimul SOLID (Single responsibility, Open-closed, Liskov substitution, Interface segregation şi Dependency inversion).

  • Principiul responsabilității unice (Single responsibility principle)

Orice context (clasă, funcție, variabilă etc.) trebuie să aibă o unică responsabilitate, iar această responsibilitate trebuie să fie în întregime încapsulată în context. Toate serviciile sale trebuie să fie orientate pentru a servi această unică responsabilitate.

O “responsabilitate” poate fi definită și ca “motiv de schimbare”. Ca exemplu, putem considera un program simplu care generează şi tipăreşte un raport. Un astfel de modul ar putea fi modificat din două motive: fie se schimbă conținutul raportului, fie se schimbă formatul raportului. Principiul responsabilității unice afirmă că aceste două aspecte ale problemei sunt două responsabilități separate şi trebuie tratate în clase sau module diferite. A trata cele două lucruri împreună în aceeași clasă sau modul ar reprezenta o problemă de design.

Astfel, este important să avem o clasă concentrată pe o unică responsabilitate pentru a o face mai robustă. Pe exemplul de mai sus, o schimbare în modul de generare a raportului ar putea produce erori în modul de tipărire, dacă aceste funcționalități fac parte din aceeaşi clasă.

  • Principiul deschis/închis (Open-closed principle)

Acest principiu implică ca entitățile software (clase, module, metode) să fie deschise pentru extensie dar închise pentru modificare Astfel, o entitate îşi poate extinde comportamentul fără modificari în codul sursă.

principiul deschis/inchis

Figura de mai sus ilustrează un contraexemplu pentru acest principiu. Introducerea unei noi entități în ierarhie (Triangle, de exemplu) ar necesita modificarea codului sursă care face desenarea.

principiu deschis/inchis

Un design care respectă acest principiu ar trebui să permită extinderea ierarhiei precum în figura de mai sus, caz în care introducerea unei noi figuri nu afectează codul existent.

  • Principiul de substituție Liskov (Liskov substitution principle)

Dacă S este un subtip al lui T, atunci obiectele de tip T pot fi substituite cu obiecte de tip S fără a afecta niciuna din proprietățile programului (corectitudine, realizarea execuției etc.).

poza3

Un exemplu tipic de încălcare a acestui principiu este o clasă Square care extinde o clasă Rectangle, presupunând că metodele getter şi setter există atât pentru lățime cât şi pentru înălțime. Clasa Square va presupune întotdeauna că lățimea şi lungimea sunt egale. Dacă vom utiliza un Square într-un context în care este aşteptat un Rectangle, putem obține un comportament neaşteptat, deoarece dimensiunile pătratului nu pot fi modificate independent.

Dacă am modifica metodele setter din clasa Square pentru a păstra invarianții, atunci aceste metode ar viola post-condițiile metodelor setter din Rectangle, care permit ca dimensiunile să fie modificate independent. Aceasta poate fi sau poate să nu fie o problemă în practică, depinzând de post-condițiile ce sunt aşteptate. Mutabilitatea este o idee cheie în acest caz. Dacă Square şi Rectangle au numai metode getter (sunt obiecte imutabile), respectarea acestui principiu este asigurată.

  • Principiul de segregare a interfețelor (Interface segregation principle)

Acest principiu afirmă că niciun client nu trebuie să fie forțat să depindă de metode pe care nu le utilizează. Interfețele trebuie separate în alte interfețe mai mici şi mai specifice.

interface Worker {
    public void work();
    public void eat();
}
class WorkerImpl implements Worker {
    public void work(){
     …
   }
   public void eat(){
     …
  }
}
class Robot implements Worker {
      public void work(){
          …
    }
     public void eat(){
         //do nothing… 
   }
}

În exemplu de mai sus, clasa Robot este forțată să implementeze metoda eat. Putem introduce o metodă care să nu facă nimic, dar pot apărea efecte nedorite în aplicație – de exemplu, rapoarte care să arate mai multe mese servite decât numărul de oameni.

interface Worker {
        public void work();
}
interface Eater {
       public void eat();
}
class WorkerImpl implements Worker, Eater {
       public void work(){
          … 
       }
       public void eat(){
          … 
       }
}
class Robot implements Worker {
        public void work(){
            … 
       }
}

Separarea interfeței Worker în două interfețe diferite are ca rezultat faptul că noua clasă Robot nu mai este forțată să implementeze metoda eat.

  • Principiul de inversare a dependențelor (Dependency Inversion principle)

Modulele de înalt nivel nu ar trebui să depindă de modulele de nivel scăzut. Ambele ar trebui să depindă de abstracții.

interface Worker {
       public void work();
}
class WorkerImpl implements Worker {
       public void work(){
             … 
       }
}
class SuperWorker implements Worker {
       public void work(){
             … 
       }
}
class Manager {
      Worker worker;
            …
}

În acest design interfața Worker reprezintă un nivel de abstracție. Ca şi consecințe:

  • Clasa Manager nu necesită schimbări atunci când sunt adăugate noi clase care implementează Worker.
  • Clasa Manager nu este afectată de schimbări şi nu este nevoie să fie retestată.

Cele  cinci principii grupate sub eticheta SOLID pot fi aplicate in implementarea de software, în faza de design, sau pot fi utilizate pentru refactoring. Este o parte a strategiei globale de dezvoltare de programe solide şi extensibile.