Table of Contents

Laboratorul 6: Visitor pattern

Video introductiv: link

Obiective

Visitor Design Pattern

Design pattern-urile reprezintă soluții generale și reutilizabile ale unei probleme comune în design-ul software. Un design pattern este o descriere a soluției sau un template ce poate fi aplicat pentru rezolvarea problemei, nu o bucată de cod ce poate fi aplicată direct. În general, pattern-urile orientate pe obiect arată relațiile și interacțiunile dintre clase sau obiecte, fără a specifica însă forma finală a claselor sau a obiectelor implicate.

Visitor este un behavioural design pattern ce oferă posibilitatea de a adăuga în mod extern funcționalități pe o întreagă ierarhie de clase, fără să fie nevoie să modificăm efectiv structura acestora.

Acest pattern este behavioural (comportamental) pentru că definește modalități de comunicare între obiecte.

Aplicabilitate

Pattern-ul Visitor este util când:

Structură

 Componente pattern Visitor

Structura design pattern-ului “Visitor” este formată din următoarele componente:

Client:

Visitor:

ConcreteVisitor:

Visitable:

ConcreteVisitable:

Flow-ul aplicării acestui pattern:

  1. Când un client dorește să efectueze operații pe obiectele vizitabile, el creează un obiect vizitator corespunzător, le “vizitează” apelând metoda accept, iar fiecare obiect vizitabil interacționează cu vizitatorul prin intermediul metodelor visit.
  2. Acest pattern oferă o modalitate de a separa algoritmii de obiectele pe care operează, facilitând extinderea și adăugarea de noi operații fără a modifica clasele obiectelor vizitabile.

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? Răspunsul vine însă chiar din situațiile în care vrem să folosim pattern-ul; vrem să lăsăm structura internă a colecţiei să facă aplicarea vizitatorilor. Cu alte cuvinte, vizitatorul se ocupă de fiecare obiect în parte, iar colecţia îl “plimbă” prin elementele sale. De exemplu, când dorim să vizităm un arbore:

  • declanşarea vizitării se va face printr-un apel accept pe un prim obiect (e.g. rădăcina arborelui)
  • elementul curent este vizitat prin apelul v.visit(this)
  • pe lângă vizitarea elementului curent, este necesar sa declanşăm vizitarea tuturor elementelor accesibile din elementul curent (e.g. nodurile-copil din arbore etc). Realizăm acest lucru apelând accept pe fiecare dintre aceste elemente. Acest comportament depinde de logica structurii.

Scenariu Visitor

Pentru a înţelege mai bine motivaţia din spatele design-pattern-ului Visitor, să considerăm următorul exemplu.

Before

Fie ierarhia de mai jos, ce defineşte un angajat (Employee) şi un şef (Manager), văzut, de asemenea, ca un angajat:

Test.java
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 Manager extends Employee {        
        float bonus;
        public Manager(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) {
                Manager manager;
                List<Employee> employees = new LinkedList<Employee>();                
                employees.add(new Employee("Alice", 20));
                employees.add(manager= new Manager("Bob", 1000));
                manager.setBonus(100);
        }
}

Ne interesează să interogăm toţi angajaţii noştri asupra venitului lor total. Observăm că:

Varianta la îndemâ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 Manager extends Employee {
        ...       
        public float getTotalRevenue() {
                return salary + bonus;
        }
}

Acum ne interesează să calculăm procentul mediu pe care îl reprezintă bonusul din venitul şefilor, luându-se în considerare doar bonusurile pozitive. Avem două posibilităţi:

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 abordare bună ar fi:

After

Conform observațiilor precedente, structura programului Employee-Manager devine:

Test.java
interface Visitor {
        public void visit(Employee employee);
        public void visit(Manager manager);
}
interface Visitable {
        public void accept(Visitor v);
}
class Employee implements Visitable {
        ...      
        public void accept(Visitor v) {
                v.visit(this);          
        }
}
class Manager 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ă:

RevenueVisitor.java
public class RevenueVisitor implements Visitor {        
        public void visit(Employee employee) {
                System.out.println(employee.getName() + " " + employee.getSalary());                
        }        
        public void visit(Manager manager) {
                System.out.println(manager.getName() + " " + (manager.getSalary() + manager.getBonus()));                
        }       
}

Secvenţele de cod de mai sus definesc:

În exemplul de mai sus, putem identifica :

Double-dispatch

Mecanismul din spatele pattern-ului Visitor poartă numele de double-dispatch. Acesta este un concept răspândit, şi se referă la faptul că metoda apelată este determinată la runtime de doi factori. În exemplul Employee-Manager, efectul vizitării, solicitate prin apelul e.accept(v), depinde de:

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

Tutorialul de double-dispatch oferă mai multe detalii legate de acest mecanism.

Cum implementăm?

  1. Se declară interfața care să reprezinte elementul nostru, care va conține și metoda public void accept(ElementVisitor elementVisitor);
  2. Se creează clasele concrete care implementează interfața declarată anterior. Body-ul pentru metoda accept va conține obligatoriu elementVisitor.visit(this);
  3. Se definește o interfață care reprezintă Visitor-ul nostru și care va conține atâtea metode de visit câte clase concrete am creat la pasul anterior (câte o metodă de visit pentru fiecare tip de element).
  4. Se creează o clasă concretă care implementează interfața de Visitor, unde vom adăuga implementările pentru fiecare tip de element în parte. (ex: ElementDisplayVisitor())
  5. În main putem testa dacă funcționează așa cum ne dorim iterând print-un arraylist/vector de obiecte de tip Element astfel: elementIterator.accept(new ElementDisplayVisitor());

Avantaje și dezavantaje

Avantaje:

Dezavantaje:

Exemple din API-uri

Visitor este de obicei utilizat pentru structuri arborescente de obiecte:

Summary

Visitor = behavioral design pattern

Exerciţii

Dorim să prelucrăm forme geometrice, pe care să le afișăm în diverse formate: text și JSON https://datatracker.ietf.org/doc/html/rfc8259. Pentru un design decuplat între formele prelucrate și tipurile de formate dorite, implementați conversia folosind patternul Visitor.

Problema de pe DevMind va avea două task-uri, corespunzătoare celor două tipuri de Visitor. Pentru simplitatea implementării acestor Visitors, vă sugerăm să urmăriți TODO-urile din schelet.

Exemplu de format text

 Circle - radius = 30

Exemplu de format JSON

 {
    "Circle": {
       "radius": 30
    }
 }

Referințe

  1. Vlissides, John, et al. Design patterns: Elements of reusable object-oriented software. Addison-Wesley (1995) (available online)