Scopul acestui laborator este prezentarea design pattern-ului Visitor și familiarizarea cu situațiile în care acesta este util de aplicat.
Design pattern-urile reprezintă soluții generale și reutilizabile ale unei probleme comune în design-ul software. Un design pattern nu este un design în forma finală, ceea ce înseamnă ca nu poate fi transformat direct în cod. Acesta este o descriere a soluției sau un template ce poate fi aplicat pentru rezolvarea problemei. In general pattern-urile orientate obiect arată relațiile și interacțiunile dintre clase sau obiecte, fără a specifica însă forma finală a claselor sau obiectelor implicate.
Design Pattern-urile fac parte din domeniul modulelor și interconexiunilor. La un nivel mai înalt se găsesc pattern-urile arhitecturale (Architectural Patterns) ce descriu structura întregului sistem.
Se consideră că exista aproximativ 2000 de design patterns [2], iar principalul mod de a le clasifica este următorul:
O carte de referință pentru design patterns este “Design Patterns: Elements of Reusable Object-Oriented Software” [1], denumită și “Gang of Four”. Aceasta definește 23 de design patterns, foarte cunoscute și utilizate în prezent. Aplicațiile pot încorpora mai multe pattern-uri pentru a reprezenta legături dintre diverse componente (clase, module). În afară de GoF, și alți autori au adus în discuție pattern-uri orientate în special pentru aplicațiile enterprise și cele distribuite.
Pattern-urile GoF sunt clasificate după următoarele tipuri:
Design pattern-ul Visitor oferă o modalitate de a separa un algoritm de structură pe care acesta operează. Avantajul constă în faptul că putem adauga noi posibilităţi de prelucrare a structurii, fără să o modificăm. Extrapolând, folosind Visitor, putem adăuga noi funcţii care realizează prelucrări asupra unei familii de clase, fără a modifica efectiv structura claselor.
Acest pattern este comportamental (behavioral) pentru că definește modalități de comunicare între obiecte.
Cum recunoaștem o situație în care Visitor e aplicabil?
Decizia de utilizare a pattern-ului Visitor este în strânsă legătură cu stabilitatea ierarhiilor de clase prelucrate: dacă noi clase copil sunt adăugate rar, atunci se poate aplica acest pattern (într-o manieră eficientă), altfel nu este indicat.
Visitor - o interfață pentru operația aplicată Visitable - o interfață pentru obiecte pe care pot aplicate operațiile (în diagramă este numită Element
)
accept
e independentă de tipul concret al Visitor-uluiaccept
se folosește obiectul de tip Visitor Pentru fiecare algoritm/operație ce trebuie aplicată, se implementează clase de tip Visitor. În fiecare obiect de tip Visitor trebuie să implementăm metode care aplică operația pentru fiecare tip de element vizitabil.
Visitor și structurile de date
Aparent, folosirea lui accept este artificială. De ce nu declanşăm vizitarea unui obiect, apelând direct v.visit(e) atunci când dorim vizitarea unui obiect oarecare? Ce se intamplă însă, când dorim să vizităm o structură complexă de obiecte? (listă, arbore, graf etc):
accept
pe un prim obiect (e.g. rădacina arborelui)v.visit(this)
accept
pe fiecare dintre aceste elemente. Acest comportament depinde de logica structurii.Traversarea structurii poate fi realizată in 3 moduri:
Pentru a înţelege mai bine motivaţia din spatele design-pattern-ului Visitor, să considerăm următorul exemplu.
Fie ierarhia de mai jos, ce defineşte un angajat (Employee) şi un şef (Boss), văzut, de asemenea, ca un angajat:
class Employee { String name; float salary; public Employee(String name, float salary) { this.name = name; this.salary = salary; } public String getName() { return name; } public float getSalary() { return salary; } } class Boss extends Employee { float bonus; public Boss(String name, float salary) { super(name, salary); bonus = 0; } public float getBonus() { return bonus; } public void setBonus(float bonus) { this.bonus = bonus; } } public class Test { public static void main(String[] args) { Boss boss; List<Employee> employees = new LinkedList<Employee>(); employees.add(new Employee("Alice", 20)); employees.add(boss = new Boss("Bob", 1000)); boss.setBonus(100); } }
Ne interesează să interogăm toţi angajaţii noştri asupra venitului lor total. Observăm că:
Varianta la indemână ar fi să definim, în fiecare din cele doua clase, câte o metodă, getTotalRevenue(), care întoarce salariul pentru angajaţi, respectiv suma dintre salariu şi bonus pentru şefi:
class Employee { ... public float getTotalRevenue() { return salary; } } class Boss extends Employee { ... public float getTotalRevenue() { return salary + bonus; } }
Acum ne interesează să calulăm procentul mediu pe care îl reprezintă bonusul din venitul şefilor, luându-se în considerare doar bonusurile pozitive. Avem două posibilităţi:
instanceof
, şi calculăm, doar pentru şefi, raportul solicitat. Dezavantajul este tratarea într-o manieră neuniformă a structurii noastre, cu evidenţierea particularităţilor fiecărei clase.Datorită acestor particularităţi (în cazul nostru, modalităţile de calcul al venitului, respectiv procentului mediu), constatăm că ar fi foarte utilă izolarea implementărilor specifice ale algoritmului (în cazul nostru, scrierea unei funcţii în fiecare clasă). Acest lucru conduce, însă, la introducerea unei metode noi în fiecare din clasele antrenate in prelucrări, de fiecare dată cand vrem să punem la dispoziţie o nouă operaţie. Obţinem următoarele dezavantaje:
În final, tragem concluzia că este de dorit să izolăm algoritmii de clasele pe care le prelucrează. O primă idee se referă la utilizarea metodelor statice. Dezavantajul acestora este că nu pot reţine, într-un mod elegant, informaţie de stare din timpul prelucrării. De exemplu, dacă structura noastră ar fi arborescentă (recursivă), în sensul că o instanţă Boss ar putea ţine referinţe la alte instanţe Boss, ce reprezintă şefii ierarhic inferiori, o funcţie de prelucrare ar trebui să menţină o informaţie parţială de stare (precum suma procentelor calculate până într-un anumit moment) sub forma unor parametri furnizaţi apelului recursiv:
class Boss extends Employee { ... public float getPercentage(float sum, int n) { float f = bonus / getTotalRevenue(); if (f > 0) return inferiorBoss.getPercentage(sum + f, n + 1); // trimite mai departe cererea catre nivelul inferior return inferiorBoss.getPercentage(sum, n); } }
O abordare mai bună ar fi:
Conform obsrevațiilor precedente, structura programului Employee-Boss devine:
interface Visitor { public void visit(Employee e); public void visit(Boss b); } interface Visitable { public void accept(Visitor v); } class Employee implements Visitable { ... public void accept(Visitor v) { v.visit(this); } } class Boss extends Employee { ... public void accept(Visitor v) { v.visit(this); } } public class Test { public static void main(String[] args) { ... Visitor v = new SomeVisitor(); // creeaza un obiect-vizitator concret for (Employee e : employees) e.accept(v); } }
Iată cum poate arăta un vizitator ce determină venitul total al fiecărui angajat şi îl afişează:
public class RevenueVisitor implements Visitor { public void visit(Employee e) { System.out.println(e.getName() + " " + e.getSalary()); } public void visit(Boss b) { System.out.println(b.getName() + " " + (b.getSalary() + b.getBonus())); } }
Secvenţele de cod de mai sus definesc:
accept(Visitor)
permite rularea unui algoritm pe structura curentă. accept(Visitor)
, în cele două clase, care, pur şi simplu, solicită vizitarea instanţei curente de către vizitator. În exemplul de mai sus, putem identifica :
Mecanismul din spatele pattern-ului Visitor poartă numele de double-dispatch. Acesta este un concept raspândit, şi se referă la faptul că metoda apelată este determinată la runtime de doi factori. În exemplul Employee-Boss, efectul vizitarii, solicitate prin apelul e.accept(v)
, depinde de:
e
(Employee sau Boss), pe care se invocă metoda v
(RevenueVisitor), care conţine implementările metodelor visitAcest lucru contrastează cu un simplu apel e.getTotalRevenue(), pentru care efectul este hotărât doar de tipul anagajatului. Acesta este un exemplu de single-dispatch.
Pattern-ul Visitor este util când:
Dezavantaje:
Visitor este de obicei utilizat pentru structuri arborescente de obiecte: