This is an old revision of the document!


Laboratorul 6: Visitor pattern

Video introductiv: link

Obiective

  • Prezentarea design pattern-ului Visitor și familiarizarea cu situațiile în care acesta este util de aplicat

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 bucata de cod ce poate fi aplicata 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 behavioral 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 behavioral (comportamental) pentru că definește modalități de comunicare între obiecte.

Aplicabilitate

Pattern-ul Visitor este util când:

  • se doreşte prelucrarea unei structuri complexe, ce cuprinde mai multe obiecte de tipuri diferite
  • se dorește definirea de operații specifice pentru aceeași structură, fără a polua interfeţele claselor implicate, cu multe detalii specifice algoritmilor. Vizitatorul centralizează logica comună, păstrând în același timp detaliile specifice în interiorul acestuia.
  • clasele ce se doresc prelucrate se modifică rar, în timp ce operaţiile de prelucrare se definesc des. Vizitatorul permite adăugarea de noi funcționalități fără modificarea claselor existente.

Structură

 Componente pattern Visitor

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

Client:

  • Este clasa consumatoare a design pattern-ului “Visitor”.
  • Are acces la obiectele din structura de date și poate instrui aceste obiecte să accepte un “Visitor” pentru a realiza prelucrările corespunzătoare.
  • Exemplu: O aplicație care procesează diferite tipuri de elemente într-o structură de date complexă.

Visitor:

  • Este o interfață sau o clasă abstractă folosită pentru a declara operațiile de vizitare pentru toate tipurile de clase vizitabile.
  • Conține metode de vizitare corespunzătoare fiecărui tip de clasă vizitabilă.
  • Exemplu: Interfața Visitor cu metodele visit(ElementA elementA), visit(ElementB elementB), etc.

ConcreteVisitor:

  • Pentru fiecare tip de “Visitor”, toate metodele de vizitare definite în “Visitor” trebuie implementate.
  • Fiecare “Visitor” este responsabil pentru diferite operații.
  • Exemplu: Clasa ConcreteVisitorA implementând interfața Visitor cu metodele sale specifice pentru tratarea diferitelor tipuri de elemente.

Visitable:

  • Este o interfață pentru obiecte pe care pot fi aplicate operațiile
  • Această operație permite unui obiect să fie “vizitat” de către un obiect “Visitor”.
  • Exemplu: Interfața Visitable cu metoda accept(Visitor visitor).

ConcreteVisitable:

  • Aceste clase implementează interfața Visitable sau clasa și definesc operația accept.
  • Prin intermediul acestei operații, obiectul “Vizitabil” primește un obiect “Visitor”.
  • Exemplu: Clasele ConcreteElementA, ConcreteElementB, etc., care implementează interfața Visitable și definesc metoda accept.

Flowul 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ădacina 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ă:

  • anagajaţii obişnuiţi au salariul ca unic venit
  • şefii posedă, pe lângă salariu, un posibil bonus

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:

  • Definim câte o metodă, getBonusPercentage(), care în Employee întoarce mereu 0, iar în Manager raportul real. Dezavantajul constă în adăugarea în interfeţe a unor funcţii prea specializate, de detalii ce ţin doar de unele implementări ale acestora.
  • Parcurgem lista de angajaţi, testăm, la fiecare pas, tipul angajatului, folosind 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 cazul unui număr mare de operaţii, interfeţele claselor se aglomerează excesiv şi se ascunde funcţionalitatea de bază a acestora
  • codul din interiorul clasei (care servea functionalităţii primare a acesteia) va fi amestecat cu cel necesar algoritmilor de prelucrare, devenind mai greu de parcurs şi întreţinut
  • în cazul în care nu avem acces la codul claselor, singura modalitate de adăugare de funcţionalitate este extinderea

În final, tragem concluzia că este de dorit să izolăm algoritmii de clasele pe care le prelucrează.

O abordare bună ar fi:

  • conceperea claselor cu posibilitatea de primire/ataşare a unor obiecte-algoritm, care definesc operaţiile dorite
  • definirea unor clase algoritm care vor vizita structura noastră de date, vor efectua prelucrările specifice fiecărei clase, având, totodată, posibilitatea de încapsulare a unor informaţii de stare (cum sunt suma şi numărul din exemplul anterior)
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:

  • o interfaţă, Visitor, ce reprezintă un algoritm oarecare, ce va putea vizita orice clasă. Observaţi definirea câte unei metode visit(…) pentru fiecare clasă ce va putea fi vizitată
  • o interfaţă, Visitable, a carei metodă accept(Visitor) permite rularea unui algoritm pe structura curentă.
  • implementări ale metodei accept(Visitor), în cele două clase, care, pur şi simplu, solicită vizitarea instanţei curente de către vizitator.
  • o implementare a unei operații aplicabilă pe obiectele de tip Visitable

În exemplul de mai sus, putem identifica :

  • Element - Visitable
  • ConcreteElement - Employee, Manager

Double-dispatch

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-Manager, efectul vizitarii, solicitate prin apelul e.accept(v), depinde de:

  • tipul elementului vizitat, e (Employee sau Manager), pe care se invocă metoda
  • tipul vizitatorului, v (RevenueVisitor), care conţine implementările metodelor visit

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:

  • Decuplarea datelor de operațiile aplicate pe acestea
  • Ușurează adăugarea unor noi operații/algortimi. Se creează o implementare a unui obiect de tip Visitor și nu se schimbă nimic în obiecte vizitate.
  • Spre deosebire de Iterator poate gestiona elemente de tipuri diferite
  • Poate menține informații de stare pe măsură ce vizitează obiectele

Dezavantaje:

  • Depinde de stabilitatea ierarhiei de obiecte vizitate. Adăugarea de obiecte vizitabile rezultă în schimbarea implementării obiectelor Visitor.
  • :!: obiecte de noi tipuri adăugate des + multe operații aplicabile = NU folosiți Visitor
  • Expune metode publice care folosesc informații de stare ale obiectelor. Nu se pot accesa membrii privați ai claselor, necesitatea expunerii acestor informaţii (in forma publică) ar putea conduce la ruperea încapsulării

Exemple din API-uri

Visitor este de obicei utilizat pentru structuri arborescente de obiecte:

  • Parcurgerea arborilor de parsare
    • ASTVisitor din Eclipse JDT. Folosind ASTParser se creează arborele de parsare al codului dat ca intrare, iar ASTVisitor parcurge arborele, oferind metode (preVisit, postVisit, visit) pentru multe tipuri de noduri (MethodDeclaration, Assignment, IfStatement etc.)
  • Parcurgerea și vizitarea ierarhiei de directoare și fișiere

Summary

Visitor = behavioral design pattern

  • Util în situații în care:
    • avem mai multe obiecte și operații pentru acestea
    • dorim schimbarea/adăugarea operațiilor fără a modifica clasele
  • Indicat de utilizat pentru operații pe colecții și parcurgerea de structuri arborescente
  • Folosește conceptul de double dispatch

Exerciţii

Dorim să prelucrăm bucăți de text pe care să le convertim în diferite formate, momentan dokuwiki și markdown. Pentru un design decuplat între obiectele prelucrate și tipurile de formate dorite, implementați conversia folosind patternul Visitor.

  • Fișierul README din scheletul de cod cuprinde informațiile necesare designului dorit.
    • implementați structura de clase din diagrama din README
    • implementați TODO-urile din scheletul de cod
  • Pentru simplitatea testării scheletul oferă clasa Test care oferă bucățile de text pe care să le prelucrați.
    • dacă folosiți IntelliJ creați proiect din scheletul de laborator: File → New Project → select Java → select the skel folder
  • În implementare va trebui sa folositi clasa StringBuilder. Aceasta este o clasă mutabilă (mutable), spre deosebire de String, care e imutabilă (immutable). Vă recomandăm acest link pentru un exemplu si explicații despre diferențele dintre ele.
  • Tips for faster coding:
    • atunci cand creati o clasa care implementeaza o interfata sau o clasa cu metode abstracte, nu scrieti de mana antetul fiecarei metode, ci folositi-va de IDE.
    • In Intellij va aparea cu rosu imediat dupa ce scrieti extends…/implements… Dati alt-enter sau option-enter (pe mac), si vi se vor genera metodele pe care trebuie sa le implementati, voi completand apoi continutul lor.
    • generati constructorii folosind IDE-ul

Referințe

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

Obiective

  • prezentarea conceptului de clasă internă
  • exemplificarea tipurilor de clase interne folosind Java
  • utilizarea claselor interne, în special ale celor anonime
  • utilizarea funcțiilor lambda

Introducere

Clasele declarate în interiorul unei alte clase se numesc clase interne (nested classes). Acestea permit gruparea claselor care sunt legate logic și controlul vizibilității uneia din cadrul celorlalte.

Clasele interne sunt de mai multe tipuri:

  • clase interne normale (regular inner classes)
  • clase anonime (anonymous inner classes)
  • clase interne statice (static nested classes)
  • clase interne metodelor (method-local inner classes) sau blocurilor

  • O clasă internă se comportă ca un membru al clasei în care a fost declarată
  • O clasă internă are acces la toți membrii clasei în care a fost declarată, inclusiv cei private
  • O clasă internă poate fi public, final, abstract dar și private, protected și static, însumând modificatorii claselor obișnuite și cei permiși metodelor și variabilelor

Clase interne "normale"

O clasă internă este definită în interiorul unei clase și poate fi accesată doar la runtime printr-o instanță a clasei externe (la fel ca metodele și variabilele ne-statice). Compilatorul creează fișiere .class separate pentru fiecare clasă internă, în exemplul de mai jos generând fișierele Car.class și Car$OttoEngine.class, însă execuția fișierului Car$OttoEngine.class nu este permisă.

Test.java
interface Engine {
    public int getFuelCapacity();
}
 
class Car {
    class OttoEngine implements Engine {
        private int fuelCapacity;
 
        public OttoEngine(int fuelCapacity) {
            this.fuelCapacity = fuelCapacity;
        }
 
        public int getFuelCapacity() {
            return fuelCapacity;
        }
    }
 
    public OttoEngine getEngine() {
        OttoEngine engine = new OttoEngine(11);
        return engine;
    }
}
 
public class Test {
    public static void main(String[] args) {
        Car car = new Car();
 
        Car.OttoEngine firstEngine = car.getEngine();
        Car.OttoEngine secondEngine = car.new OttoEngine(10);
 
        System.out.println(firstEngine.getFuelCapacity());
        System.out.println(secondEngine.getFuelCapacity());
    }
}
student@poo:~$ javac Test.java
student@poo:~$ ls
Car.class Car$OttoEngine.class Engine.class Test.class Test.java

Urmăriți exemplul de folosire a claselor interne de mai sus. Adresați-vă asistentului pentru eventuale neclarități.

Dintr-o clasă internă putem accesa referința la clasa externă (în cazul nostru Car) folosind numele acesteia și keyword-ul this:

Car.this;

Modificatorii de acces pentru clase interne

Așa cum s-a menționat și în secțiunea Introducere, claselor interne le pot fi asociați orice identificatori de acces, spre deosebire de clasele top-level Java, care pot fi doar public sau package-private. Ca urmare, clasele interne pot fi, în plus, private și protected, aceasta fiind o modalitate de a ascunde implementarea.

Folosind exemplul anterior, dacă modificăm clasa OttoEngine pentru a fi privată apar erori de compilare.

  • Tipul Car.OttoEngine nu mai poate fi accesat din exterior. Acest neajuns poate fi rezolvat cu ajutorul interfeței Engine. Asociindu-i clasei interne Car.OttoEngine supertipul Engine prin moștenire, putem instanția clasa prin upcasting.
  • Fiind privată, clasa internă are implicit toți constructorii privați. Totuși, putem instanția obiecte de tipul Car.OttoEngine în interiorul clasei Car, urmând să le întoarcem folosind tot upcasting la Engine. Astfel, folosind metode getEngine, ascundem complet implementarea clasei Car.OttoEngine.

Clase anonime

În dezvoltarea software, există situații când o componentă a aplicației are o utilitate suficient de mare pentru a putea fi considerată o entitate separată (sau clasă). De multe ori, însă, aceasta nu este utilizată decât într-o porțiune restrânsă din aplicație, într-un context foarte specific (într-un lanț de moșteniri sau ierarhie de interfețe).

Putem folosi clase interne anonime în locul definirii unei clase cu număr de utilizări reduse. Acestea nu au nume și apar în program ca instanțe ale unei clase moștenite (sau a unei interfețe extinse), care suprascriu (sau implementează) anumite metode.

Întorcându-ne la exemplul cu clasa top-level Car, putem rescrie metoda getEngine() a acesteia astfel:

[...]
class Car {
    public Engine getEngine(int fuelCapacity) {
        return new Engine () {
            private int fuelCapacity = 11;
 
            public int getFuelCapacity() {
                return fuelCapacity;
            }
        };
    }
}
[...]

Metoda folosită mai sus elimină necesitatea creări unei clase interne “normale”, reducând volumul codului și crescând lizibilitatea acestuia. Sintaxa return new Engine() { … } se poate citi astfel: “Crează o clasă care implementează interfața Engine, conform următoarei implementări”.

Observații:

  • Obiectul este instanțiat imediat după return, folosind new (referința întoarsă de new va fi upcast la tipul de bază: Engine)
  • Numele clasei instanțiate este absent (ea este anonimă), însă ea este de tipul Engine, prin urmare, va implementa metoda/metodele din interfață (cum e metoda getFuelCapacity). Corpul clasei urmează imediat instanțierii.

Limitări

  • Clasele anonime nu pot avea constructori din cauză că nu au nume (nu am ști cum să numim constructorii). Această restricție asupra claselor anonime ridică o problemă: în mod implicit, clasă de bază este creată cu constructorul default.
  • O clasă internă anonimă poate extinde o clasă sau să implementeze o singură interfață, nu poate face pe ambele împreună ca la clasele ne-anonime (interne sau nu), și nici nu poate să implementeze mai multe interfețe.

Clasele interne anonime declarate în metode pot folosi variabilele declarate în metoda respectivă și parametrii metodei dacă aceștia sunt final sau effectively final. Dacă o variabilă nu e declarată final dar nu se modifică după inițializare, atunci este effectively final.

Variabilele si parametrii metodelor se află pe segmentul de stivă din memorie creat pentru metoda respectivă, ceea ce face ca ele să nu existe la fel de mult cât clasa internă. Dacă variabila este declarată final, atunci la runtime se va stoca o copie a acesteia ca un câmp al clasei interne, în acest mod putând fi accesată și după execuția metodei.

Până la Java 8 nu exista conceptul de effectively final si toate aceste variabile trebuiau declarate final

Expresii Lambda

Când veți scrie o clasă anonimă într-un IDE cum e IntelliJ, veți primi un warning care vă recomandă să o transformați într-o lambda. Aceasta recomandare este valabilă doar pentru implementarea claselor/interfețelor cu o singură metoda.

Lambda este un concept din programarea funcțională (o să îl învățați în semestrul 2 la Paradigme de Programare) și reprezintă o funcție anonimă. Majoritatea limbajelor de nivel înalt au introdus suport pentru acest concept în ultimii 15 ani, inclusiv Java, din versiunea 8.

În exemplul următor am transformat clasa anonimă din secțiunea precedentă. Atenție! IDE-ul nu ar fi recomandat pentru cazul din acel exemplu transformarea în lambda pentru că aveam definită o variabilă private int fuelCapacity = 11, deci codul nu era echivalent unei simple implementări de funcții. Eliminănd acea variabilă constantă și punând-o în metodă am putut face transformarea.

class Car {
    public Engine getEngine() {
        return () -> 11; // expresie lambda
    }
}

În codul de mai sus declararea clasei anonime și suprascrierea metodei din getFuelCapacity() a fost înlocuită cu o expresie lambda. O altă situație des intalnită de folosire a lambda-urilor este pentru transmiterea de funcții ca parametru iar api-uri precum cel de filtrare al colecțiilor le utilizează intens (exemplu).

În exemplul de mai jos aveți niște exemple simple de folosire lambda ca paremetru pentru metode. Parametrii primiți în lambda pot fi zero (() ca în exemplul cu fuelCapacity) sau mai mulți (param1, param2, …).

    public static void main(String[] args) {
 
        ArrayList<Integer> values = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
 
        values.removeIf((v) -> v % 2 == 0);           // parametru o lambda care intoarce daca numărul primit este par sau nu
 
        values.forEach((v) -> System.out.println(v)); // parametru o lambda care afiseaza argumentul primit
        values.forEach(System.out::println);          // referinta catre metoda println
    }
}

Operatorul :: este folosit pentru referințierea metodelor.

O să mai oferim exemple și detalii despre lambdas și în laboratoarele următoare, de exemplu cel cu Colecții sau cel pentru concepte din Java 8.

Clase interne statice

În secțiunile precedente, s-a discutat despre clase interne ale caror instanțe există doar în contextul unei instanțe a clasei exterioare, astfel că pot accesa membrii obiectului exterior direct.

Clasele interne le vedem ca pe membri ai claselor exterioare, așa cum sunt câmpurile și metodele. De accea, ele pot lua toți modificatorii disponibili membrilor, printre ei aflându-se modificatori pe care clasele exterioare nu le pot avea (e.g. private, static).

Așa cum pentru a accesa metodele și variabilele statice ale unei clase nu este nevoie de o instanță a acesteia, putem obține o referință către o clasă internă fără a avea nevoie de o instanță a clasei exterioare.

Pentru clasele interne statice:

  • Nu avem nevoie de un obiect al clasei externe pentru a crea un obiect al clasei interne
  • Nu putem accesa câmpuri nestatice ale clasei externe din clasă internă (nu avem o instanță a clasei externe)

Clasele interne statice nu au nevoie de o instanță a clasei externe → atunci de ce le facem interne acesteia?

  • Pentru a grupa clasele, dacă o clasă internă statică A.B este folosită doar de A, atunci nu are rost să o facem top-level.

Avem o clasă internă MyOuterClass.MyInnerClass, de ce să o facem statică?

  • Dacă în interiorul clasei MyInnerClass nu avem nevoie de nimic specific instanței clasei externe MyOuterClass, deci nu avem nevoie de o instanță a acesteia o putem face statică

Clase interne în metode și blocuri

Primele exemple prezintă modalitățile cele mai uzuale de folosire a claselor interne. Totuși, design-ul claselor interne este destul de complex și exista modalitati mai “obscure” de a le folosi: clasele interne pot fi definite și în cadrul metodelor sau al unor blocuri arbitrare de cod.

Click aici pentru exemplu de clasă internă declarată în metodă

Click aici pentru exemplu de clasă internă declarată în metodă

În exemplul următor, clasa internă a fost declarată în interiorul funcției getEngine. În acest mod, vizibilitatea ei a fost redusă pentru ca nu poate fi instanțiată decât în această funcție.

Singurii modificatori care pot fi aplicați acestor clase sunt abstract și final (binențeles, nu amândoi deodată).

Pentru a accesa variabile declarate în metodă respectivă sau parametri ai acesteia, ele trebuie să fie final. Vedeți explicația în secțiunea despre clasele anonime.

Test.java
[...]
class Car {
    public Engine getEngine() {
        class OttoEngine implements Engine {
            private int fuelCapacity = 11;
 
            public int getFuelCapacity() {
                return fuelCapacity;
            }
        }
 
        return new OttoEngine();
    }
}
[...]

Click aici pentru exemplu de clasă internă declarată în bloc

Click aici pentru exemplu de clasă internă declarată în bloc

Exemplu de clasa internă declarata într-un bloc:

[...]
class Car {
    public Engine getEngine(int fuelCapacity) {
        if (fuelCapacity == 11) {
            class OttoEngine implements Engine {
                private int fuelCapacity = 11;
 
                public int getFuelCapacity() {
                    return fuelCapacity;
                }
            }
 
            return new OttoEngine();
        }
 
        return null;
    }
}
[...]

În acest exemplu, clasa internă OttoEngine este defintă în cadrul unui bloc if, dar acest lucru nu înseamnă că declarația va fi luată în considerare doar la rulare, în cazul în care condiția este adevarată.

Semnificația declarării clasei într-un bloc este legată strict de vizibilitatea acesteia. La compilare clasa va fi creată indiferent care este valoarea de adevăr a condiției if.

Moștenirea claselor interne

Deoarece constructorul clasei interne trebuie sa se atașeze de un obiect al clasei exterioare, moștenirea unei clase interne este puțin mai complicată decât cea obișnuită. Problema rezidă în nevoia de a inițializa legătura (ascunsă) cu clasa exterioară, în contextul în care în clasa derivată nu mai există un obiect default pentru acest lucru.

class Car {
    class Engine {
        public void getFuelCapacity() {
            System.out.println("I am a generic Engine");
        }
    }
}
 
class OttoEngine extends Car.Engine {
    OttoEngine() {
    } // EROARE, avem nevoie de o legatura la obiectul clasei exterioare
 
    OttoEngine(Car car) { // OK
        car.super();
    }
}
 
public class Test {
    public static void main(String[] args) {
        Car car = new Car();
        OttoEngine ottoEngine = new OttoEngine(car);
        ottoEngine.getFuelCapacity();
    }
}

Observăm ca OttoEngine moștenește doar Car.Engine însă sunt necesare:

  • parametrul constructorului OttoEngine trebuie să fie de tipul clasei externe (Car)
  • linia din constructorul OttoEngine: car.super().

Utilizarea claselor interne

Clasele interne pot părea un mecanism greoi și uneori artificial. Ele sunt însă foarte utile în următoarele situații:

  • Rezolvăm o problemă complicată și dorim să creăm o clasă care ne ajută la dezvoltarea soluției dar:
    • nu dorim să fie accesibilă din exterior sau
    • nu mai are utilitate în alte zone ale programului
  • Implementăm o anumită interfață și dorim să întoarcem o referință la acea interfață, ascunzând în același timp implementarea.
  • Dorim să folosim/extindem funcționalități ale mai multor clase, însă în JAVA nu putem extinde decât o singură clasă. Putem defini însă clase interioare. Acestea pot moșteni orice clasă și au, în plus, acces la obiectul clasei exterioare.
  • Implementarea unei arhitecturi de control, marcată de nevoia de a trata evenimente într-un sistem bazat pe evenimente. Unul din cele mai importante sisteme de acest tip este GUI (graphical user interface). Bibliotecile Java Swing, AWT, SWT sunt arhitecturi de control care folosesc intens clase interne. De exemplu, în Swing, pentru evenimente cum ar fi apăsarea unui buton se poate atașa obiectului buton o tratare particulară al evenimentului de apăsare în felul următor:
 button.addActionListener(new ActionListener() { //interfata implementata e ActionListener
	public void actionPerformed(ActionEvent e) {            
	     numClicks++;
	}
    });

Exerciții

Schelet de laborator: Laborator6

Exercițiile din acest laborator au ca scop simularea obținerii prețului unei mașini de la un dealer. Construcția obiectelor necesare o veți face de la zero conform instrucțiunilor din taskuri.

Task 1 - Structura de bază (2p)

Car Creați clasa Car cu următoarele proprietăți: prețul, tipul și anul fabricației.

  • Tipul este reprezentat printr-un enum enum CarType declarat intern în Car. Acesta conține trei valori: Mercedes, Fiat și Skoda.
  • Prețul și anul vor fi de tipul integers.

Creați un constructor cu toti cei trei parametri, în ordinea din enunț și suprascrieți metoda toString() pentru afișare în felul următor: Car{price=20000, carType=SKODA, year=2019}

Offer

Creați interfața Offer ce conține metoda: int getDiscount(Car car);.

Dealership

Creați clasa Dealership care se va ocupa cu aplicarea ofertelor pentru mașini.

Task 2 - Ofertele (4p)

În clasa Dealership creați trei clase interne private care implementează Offer.

  • BrandOffer - calculează un discount în funcție de tipul mașinii:
    1. Mercedes: discount 5%;
    2. Fiat: discount 10%;
    3. Skoda: discount 15%;
  • DealerOffer - calculează un discount în funcție de vechimea mașinii:
    1. Mercedes: discount 300 pentru fiecare an de vechime;
    2. Fiat: discount 100 pentru fiecare an de vechime;
    3. Skoda: discount 150 pentru fiecare an de vechime;
  • SpecialOffer - calculează un discount random, cu seed 20. Generarea se va realiza în constructor utilizându-se o instanța globală a unui obiect de tip Random care a fost inițializat cu seed-ul 20 și cu limita superioară (bound) 1000 Random.

Adăugați o metodă în clasa Dealership care oferă prețul mașinii după aplicarea discount-urilor din oferte: getFinalPrice(Car car)

  • aplicați pe obiectul car primit ca argument cele trei oferte in ordinea: BrandOffer, DealerOffer, SpecialOffer.
  • metoda va returna prețul final după aplicarea ofertelor

Testare oferte: Creati 2 obiecte Car pentru fiecare tip de mașină cu urmatoarele valori:.

  1. Mercedes:
    1. Pret: 20000, An: 2010;
    2. Pret: 35000, An: 2015;
  2. Fiat:
    1. Pret: 3500, An: 2008;
    2. Pret: 7000, An: 2010;
  3. Skoda:
    1. Pret: 12000, An: 2015;
    2. Pret: 25000, An: 2021;
  • Creati un obiect de tip Dealership.
  • Obțineți și afișați prețul oferit de Dealership(folosind metoda getFinalPrice) pentru fiecare obiect.
  • De fiecare data cand se aplica o oferta asupra unui obiect de tip Car se va afisa un mesaj de tipul: “Applying x discount: y euros”, unde:
    • x reprezinta oferta care a fost aplicata(Brand, Dealer, Special, Client)
    • y reprezinta discount-ul ofertei.

Task 3 - Negocierea (2p)

Aăugați în clasa Dealership metoda void negotiate(Car car, Offer offer). Aceasta permite clientului să propună un discount.

În metoda main apelați negotiate dând ca parametru oferta sub formă de clasă anonimă. Implementarea ofertei clientului reprezinta returnarea unui discount de 5%. Pentru testare folositi urmatorul obiect Car: -Pret: 20000 -Tip: Mercedes -An: 2019

Task 4 - Lambda (2p)

Testați folosirea expresiilor lambda pe următorul caz: pe o listă de obiecte de tip Car cu prețuri variate, eliminați toate mașinile care au prețul peste 25000. Afișați lista înainte și după modificare. Pentru lista folositi urmatoarele obiecte Car:

  1. Mercedes:
    1. Pret: 30000, An: 2019;
    2. Pret: 50000, An: 2021;
  2. Fiat:
    1. Pret: 10000, An: 2018;
  3. Skoda:
    1. Pret: 20000, An: 2019;

Resurse

Referințe

  1. Kathy Sierra, Bert Bates. SCJP Sun Certified Programmer for Java™ 6 - Study Guide. Chapter 8 - Inner Classes (available online)
poo-ca-cd/laboratoare/clase-interne.1731249411.txt.gz · Last modified: 2024/11/10 16:36 by silvia_elena.nistor
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0