CATEGORII DOCUMENTE |
Reutilizarea codului in POO
Reutilizarea codului prin delegare
Unul din avantajele programarii orientate pe obiecte este posibilitatea de reutilizare simpla si sigura a unor clase existente in definirea altor clase, fara a modifica clasele initiale. Altfel spus, o tehnica specifica POO este adaugarea de noi metode unor clase existente, simultan cu reutilizarea unor metode mostenite. Metodele de reutilizare a codului sunt delegarea si derivarea, iar schema de proiectare ce realizeaza extinderea functionalitatii unei clase se numeste "decorator", de la imaginea unor decoratiuni adaugate unui obiect existent pentru a-i spori utilitatea sau estetica.
In cazul delegarii o clasa A contine o variabila de un tip clasa B, pentru reutilizarea functionalitatii clasei B in noua clasa A, prin delegarea catre metodele clasei B a metodelor clasei A.
Clasa urmatoare din colectiile Google ilustreaza esenta delegarii pentru "decorarea" unei clase:
public abstract class ForwardingObject implements Serializable
protected Object delegate()
@Override public String toString()
}
De remarcat ca nu sunt delegate si operatiile "equals" sau "hashCode", care vor fi definite in subclasele acestei clase abstracte.
In exemplul urmator se defineste o clasa pentru stive prin delegarea operatiilor cu stiva catre metodele listei inlantuite de obiecte LinkedList:
public class LinkedStack
public Object push (Object obj)
public Object pop ()
public boolean isEmpty ()
public String toString ()
}
De observat ca tipurile "LinkedStack" si LinkedList nu sunt compatibile si nu se pot face atribuiri intre ele, desi continutul claselor este atat de asemanator. Varianta definirii clasei 'LinkedStack' ca o subclasa a clasei LinkedList este preferabila aici, mai ales ca doua dintre metode pot fi mostenite ca atare ('isEmpty' si 'toString').
Clasa "LinkedStack" este numita si clasa "adaptor" pentru ca face trecerea de la un set de metode publice ("addFirst","removeFirst" ale clasei LinkedList) la un alt set de metode publice ("push", "pop" etc.).
Delegarea de metode intre obiecte poate fi o relatie dinamica, modificabila la executie, spre deosebire de relatia statica de derivare, stabilita la scrierea programului si care nu mai poate fi modificata. Clasa compusa poate contine o variabila de un tip interfata sau clasa abstracta, care poate fi inlocuita la executie (la construirea unui obiect, de exemplu) printr-o referinta la un obiect de un alt tip, compatibil cu tipul variabilei din clasa.
Vom relua exemplul cu stiva adaugand un grad de generalitate prin posibilitatea utilizatorului de a-si alege tipul de lista folosit pentru stiva (vector sau lista inlantuita):
public class StackList
public Object push (Object obj)
. . . // alte metode
}
La construirea unui obiect "StackList" trebuie precizat tipul de lista folosit (vector sau altceva):
StackList st1 = new StackList (new ArrayList()); // vector
StackList st2 = new StackList (new LinkedList()); // lista inlantuita
In exemplul urmator se defineste o clasa dictionar cu valori multiple, in care fiecare cheie are asociata o lista de valori. Clasa preia o mare parte din functionalitatea clasei HashMap, prin delegarea unor operatii catre metode din clasa HashMap.
public class MultiMap
public List get (Object key)
public String toString ()
return str;
}
public Set keySet ()
}
Delegarea se mai numeste "agregare" sau "compozitie": clasa agregat (compusa) contine unul sau mai multe obiecte catre care deleaga anumite operatii. In UML se face distinctie intre termenii "compozitie" si "agregare": in compozitie distrugerea unui obiect compus are ca urmare distrugerea obiectele continute (care nu mai pot exista independent de obiectul care le contine). In Java nu se face aceasta diferenta doarece obiectul compus contine referinte la obiectele componente, care pot exista si dupa disparitia obiectului compus (desi s-ar putea pierde referintele la ele).
Reutilizarea codului prin derivare
Prin derivare se face o adaptare sau o specializare a unei clase mai generale la anumite cerinte particulare fara a opera modificari in clasa initiala. Extinderea unei clase permite reutilizarea unor metode din superclasa, fie direct, fie dupa 'ajustari' si 'adaptari' cerute de rolul subclasei. Superclasa transmite subclasei o mare parte din functiile sale, nefiind necesara rescrierea sau apelarea metodelor mostenite.
Vom relua exemplul clasei pentru stive realizate ca liste inlantuite. Operatiile cu o stiva se numesc traditional "push" si "pop", dar clasa LinkedList foloseste alte nume pentru operatiile respective. De aceea, vom defini doua metode noi:
public class LinkedStack extends LinkedList
public Object pop ()
}
De observat ca subclasa "LinkedStack" mosteneste metodele 'toString', 'isEmpty' si altele din clasa LinkedList. De asemenea, exista un constructor implicit care apeleaza constructorul superclasei si care initializeaza lista stiva.
O problema in acest caz ar putea fi posibilitatea utilizatorilor de a folosi pentru obiecte 'LinkedStack' metode mostenite de la superclasa, dar interzise pentru stive: citire si modificare orice element din stiva, cautare in stiva s.a. Solutia este de a redefini aceste metode in subclasa, cu efectul de aruncare a unor exceptii. Exemplu:
public Object remove (int index)
Iata si o alta solutie de definire clasei 'MultiMap' (dictionar cu valori multiple), pe baza observatiei ca lista de valori asociata unei chei (de tip List) este tot un obiect (compatibil cu tipul Object) si deci se poate pastra interfata publica a clasei HashMap:
public class MultiMap extends HashMap
}
Comparatie intre compozitie si derivare
Delegarea (un B contine un A) se recomanda atunci cand vrem a folosim (sa reutilizam) functionalitatea unei clase A in cadrul unei clase B, dar interfetele celor doua clase sunt diferite.
Derivarea (un B este un fel de A) se recomanda atunci cand vrem sa reutilizam o mare parte din (sau toata) interfata clasei A si pentru clasa B. In plus, derivarea creeaza tipuri compatibile: putem sa inlocuim o variabila sau un argument de tipul A printr-o variabila (sau argument) de tipul B .
Atat derivarea cat si compozitia permit reutilizarea metodelor unei clase, fie prin mostenire, fie prin delegare (prin apelarea metodelor obiectului continut).
Derivarea din clase instantiabile sau abstracte este solutia folosita in cazul unor infrastructuri de clase ("Frameworks") pentru crearea unor ierarhii de tipuri compatibile: clasele colectii de Object, clase de intrare-iesire s.a. In felul acesta clasele definite de programatori pot mosteni un numar mare de metode (si date) cu un efort minim de programare. Exemple:
- O noua clasa colectie poate mosteni de la AbstractCollection (AbstractSet, AbstractList, AbstractMap) o mare parte din metode, la care adauga cateva metode specifice noii clase;
- O clasa pentru o interfata grafica Swing extinde de obicei clasa JFrame de la care mosteneste metode de adaugare a unor componente Swing, de stabilire asezare ("Layout") si de vizualizare;
- O clasa aplet (sau servlet) este definita de programator prin extinderea unei clase de biblioteca (JApplet, de exemplu) de la care mosteneste metode absolut necesare functionarii sale ca aplet.
- O clasa ce defineste un fir de executie Java nu se poate defini decat prin extinderea clasei de biblioteca Thread sau prin implementarea interfetei Runnable.
Aceste exemple (si altele) pot fi interpretate si astfel: o parte din functionalitatea unei aplicatii este realizata prin metode ale unor clase predefinite (de biblioteca), iar o alta parte prin metode ale unor clase definite de programatori si care este specifica fiecarei aplicatii (sau parti de aplicatie). In plus, unele din metodele definite de programatori trebuie sa respecte anumite interfete impuse, pentru ca sunt apelate din alte clase predefinite (metoda "run" dintr-o subclasa a clasei Thread, metoda "paint" dintr-o subclasa a clasei JApplet, s.a.).
O situatie care apare la utilizarea unor API standard, cum ar fi DOM (XML), este aceea ca nu se cunosc decat nume de interfete si nu se cunosc nume de clase care implementeaza aceste interfete (pot fi mai multe astfel de implementari si chiar unele care urmeaza sa apara in viitor). Daca vrem sa redefinim o metoda dintr-o astfel de clasa anonima (cum ar fi metoda "toString" asociata unui nod de arbore DOM, definit prin interfata Node) nu putem folosi derivarea si ramane doar delegarea.
Delegarea se foloseste la definirea unor clase adaptor de la o interfata la o alta interfata dar si atunci cand tipul obiectului delegat se poate modifica la executie (adresa obiectului este primita de constructorul fiecarui obiect). Un exemplu este cel al claselor flux de I/E care deleaga operatiile elementare de citire/scriere de caractere individuale catre obiecte ce depind de suportul fizic al fluxului de date (fisier disc, o zona de memorie, etc.).
Un alt exemplu este cel al claselor flux de date cu suma de control, care contin o variabila interfata (Checksum), care va primi la instantiere adresa obiectului ce contine metoda de calcul a sumei de control. Legatura dintre un obiect flux si obiectul de calcul a sumei este stabilita la executie si asigura o flexibilitate sporita. Exemplu:
public class CheckedOutputStream extends FilterOutputStream
public void write(int b) throws IOException
. . .
}
// Exemplu de utilizare
CRC32 Checker = new CRC32(); // o clasa care implementeaza interfata Checksum
CheckedOutputStream out;
out = new CheckedOutputStream (new FileOutputStream('date'), Checker);
while (in.available() > 0)
Colectiile Google folosesc mai mult delegarea ca metoda de reutilizare, comparativ cu clasele colectie din Java care se bazeaza mai mult pe derivare.
De cele mai multe ori metoda de reutilizare a unor clase se impune de la sine, dar uneori alegerea intre compozitie si derivare nu este evidenta si chiar a condus la solutii diferite in biblioteci de clase diferite. Un exemplu este cel al claselor pentru vectori si respectiv pentru stive. Ce este o stiva ? Un caz particular de vector sau un obiect diferit care contine un vector ?
In Java clasa Stack este derivata din clasa Vector, desi un obiect stiva nu foloseste metodele clasei Vector ( cu exceptia metodei 'toString'). Mai mult, nici nu se recomanda accesul direct la orice element dintr-o stiva (prin metodele clasei Vector).
In Java o subclasa nu poate avea decat o singura superclasa, dar uneori este necesar ca o clasa sa preia functii de la doua sau mai multe clase diferite. Implementarea mai multor interfete de catre o clasa nu este o solutie pentru mostenirea multipla de functii.
O clasa M poate prelua metode de la doua clase A si B astfel: clasa M extinde pe A si contine o variabila de tip B; metodele din M fie apeleaza metode din clasa B, fie sunt mostenite de la clasa A. Clasa A este de multe ori o clasa abstracta, iar clasa B este instantiabila sau abstracta. Exemplu de mostenire functii de la 3 clase:
class A
}
class B
}
class C
}
class M extends C // delegare obiect a pentru operatia f1
public void f2 () // delegare obiect b pentru operatia f2
}
class X
}
Un exemplu real de mostenire multipla poate fi o clasa pentru o multime realizata ca vector, care extinde clasa AbstractSet si contine o variabila de tip ArrayList :
public class ArraySet extends AbstractSet
public boolean add (Object obj)
public Iterator iterator()
public int size()
}
Pentru multimi de tipul 'ArraySet' se pot folosi toate metodele mostenite de la clasa AbstractSet: toString, contains, containsAll, addAll, removeAll, retainAll s.a.
Aceeasi solutie de mostenire multipla este folosita in cateva clase JFC (Swing) de tip "model"; de exemplu, clasa DefaultListModel preia metode de la superclasa AbstractListModel si deleaga unei variabile interne de tip Vector operatii cu un vector de obiecte. Un model de lista este un vector cu posibilitati de generare evenimente (de apelare receptori) la modificari operate in vector.
Combinarea compozitiei cu derivarea
Derivarea pastreaza interfata clasei de baza, iar delegarea permite mai multa flexibilitate la executie. Combinarea delegarii cu derivarea imbina avantajele ambelor metode de reutilizare: mostenirea interfetei si posibilitatea modificarii obiectului delegat.
Uneori variabila din subclasa este chiar de tipul superclasei, tip clasa abstracta sau interfata. Totusi nu se justifica o constructie de forma urmatoare, unde A este o clasa instantiabila:
class B extends A
. . . // metode ale clasei B
}
Clasele ce contin un obiect de acelasi tip sau de un tip compatibil cu al superclasei sale se mai numesc si clase "anvelopa" ("wrapper"), deoarece adauga functii obiectului continut, ca un ambalaj pentru acel obiect. O clasa anvelopa este numita si clasa "decorator", deoarece 'decoreaza' cu noi functii o clasa existenta.
De exemplu, putem defini o clasa stiva mai generala, care sa poata folosi fie un vector, fie o lista inlantuita, dupa cum doreste programatorul. Clasa anvelopa care urmeaza este compatibila cu tipul List si, in acelasi timp, foloseste metode definite in clasa AbstractList :
class StackList extends AbstractList
public Object push (Object obj)
public Object pop ()
public int size()
}
Exemple de combinare a derivarii si compozitiei se gasesc in clasa Collections, pentru definirea de clase colectie cu functionalitate putin modificata fata de colectiile uzuale, dar compatibile cu acestea ca tip. Primul grup de clase este cel al colectiilor nemodificabile, care difera de colectiile generale prin interzicerea operatiilor ce pot modifica continutul colectiei. Exemplu:
class UnmodifiableCollection implements Collection, Serializable
public boolean isEmpty()
public boolean contains(Object o)
public String toString()
. . .
// metode care ar putea modifica continutul colectiei
public boolean add (Object o)
public boolean remove (Object o)
. . .
}
Incercarea de adaugare a unui nou obiect la o colectie nemodificabila produce o exceptie la executie; daca nu se defineau metodele 'add', 'remove' s.a. in clasa derivata, atunci apelarea metodei 'add' era semnalata ca eroare la compilare. Exceptia poate fi tratata fara a impiedica executia programului.
Al doilea grup de clase sunt clasele pentru colectii sincronizate. Exemplu:
class SynchronizedCollection implements Collection, Serializable
}
public boolean add(Object o)
}
. . . // alte metode
}
In realitate, clasele prezentate sunt clase incluse statice si sunt instantiate in metode statice din clasa Collections:
static class UnmodifiableList extends UnmodifiableCollection implements List
. . . // alte metode
}
public static List unmodifiableList (List list)
Exemple de utilizare a claselor colectie 'speciale' :
String kw[] =;
List list = Collections.unmodifiableList (Arrays.asList(kw));
Set s = Collections.synchronizedSet (new HashSet());
Clase decorator de intrare-iesire
Combinarea derivarii cu delegarea a fost folosita la proiectarea claselor din pachetul "java.io". Exista doua familii de clase paralele : familia claselor "flux" ("Stream") cu citire-scriere de octeti si familia claselor "Reader-Writer", cu citire-scriere de caractere.
Numarul de clase instantiabile de I/E este relativ mare deoarece sunt posibile diverse combinatii intre suportul fizic al fluxului de date si facilitatile oferite de fluxul respectiv. Toate clasele flux de intrare sunt subtipuri ale tipului InputStream (Reader) si toate clasele flux de iesire sunt subtipuri ale tipului OutputStream (Writer). Clasele Reader, Writer si celelalte sunt clase abstracte.
Dupa suportul fizic al fluxului de date se poate alege intre:
- Fisiere disc : FileInputStream, FileOutputStream, FileReader, FileWriter s.a.
- Vector de octeti sau de caractere : ByteArrayInputStream, ByteArrayOutputStream CharArrayReader, CharArrayWriter.
- Buffer de siruri (in memorie): StringBufferInputStream, StringBufferOutputStream.
- Canal pentru comunicarea sincronizata intre fire de executie : PipedInputStream, PipedOutputStream, PipedReader, PipedWriter.
Dupa facilitatile oferite avem de ales intre:
- Citire-scriere la nivel de octet sau bloc de octeti (metode "read", "write").
- Citire-scriere pe octeti dar cu zona buffer: BufferedInputStream, BufferedReader, BufferedOutputStream, BufferedWriter.
- Citire-scriere la nivel de linie si pentru numere de diferite tipuri (fara conversie): DataInputStream, DataOutputStream..
- Citire cu punere inapoi in flux a ultimului octet citit (PushBackInputStream).
- Citire insotita de numerotare automata a liniilor citite (LineNumberInputStream).
Combinarea celor 8 clase sursa/destinatie cu optiunile de prelucrare asociate transferului de date se face prin intermediul claselor anvelopa , care sunt numite si clase "filtru" de intrare-iesire. Daca s-ar fi utilizat derivarea pentru obtinerea claselor direct utilizabile atunci ar fi trebuit generate, prin derivare, combinatii ale celor 8 clase cu cele 4 optiuni, deci 32 de clase (practic, mai putine, deoarece unele optiuni nu au sens pentru orice flux). Pentru a folosi mai multe optiuni cu acelasi flux ar fi trebuit mai multe niveluri de derivare si deci ar fi rezultat un numar si mai mare de clase.
Solutia claselor anvelopa permite sa se adauge unor clase de baza diverse functii in diferite combinatii. Principalele clase filtru de intrare-iesire sunt:
FilterInputStream, FilterOutputStream si FilterReader, FilterWriter.
Clasele de tip filtru sunt clase intermediare, din care sunt derivate clase care adauga operatii specifice (de "prelucrare"): citire de linii de text de lungime variabila, citire-scriere de numere in format intern, scriere numere cu conversie de format s.a. O clasa anvelopa de I/E contine o variabila de tipul abstract OutputStream sau InputStream, care va fi inlocuita cu o variabila de un tip flux concret (FileOutputStream, ), la construirea unui obiect de un tip flux direct utilizabil.
Clasa decorator FilterInputStream este derivata din InputStream si, in acelasi timp, contine o variabila de tip InputStream:
public class FilterInputStream extends InputStream
// citirea unui octet
public int read () throws IOException
// alte metode
}
Metoda 'read' este o metoda polimorfica, iar selectarea metodei necesare se face in functie de tipul concret al variabilei 'in' (transmis ca argument constructorului). Nu se pot crea obiecte de tipul FilterInputStream deoarece constructorul clasei este de tip protected, dar se pot crea obiecte din subclase ale clasei FilterInputStream. Clasele DataInputStream, BufferedInputStream, PushbackInputStream, LineNumberInputStream si sunt derivate din clasa FilterInputStream si sunt clasele de prelucrare a datelor citite. Cea mai folosita este clasa DataInputStream care adauga metodelor de citire de octeti mostenite si metode de citire a tuturor tipurilor primitive de date: 'readInt', 'readBoolean', readFloat', 'readLine', etc.
La crearea unui obiect de tipul DataInputStream constructorul primeste un argument de tipul InputStream, sau un tip derivat direct din InputStream, sau de un tip derivat din FilterInputStream. Pentru a citi linii dintr-un fisier folosind o zona tampon, cu numerotare de linii vom folosi urmatoarea secventa de instructiuni:
public static void main (String arg[]) throws IOException
De obicei nu se mai folosesc variabile intermediare la construirea unui obiect flux. Exemplu de citire linii , cu buffer, dintr-un fisier disc:
public static void main (String arg[ ]) throws IOException
Ordinea in care sunt create obiectele de tip InputStream este importanta : ultimul obiect trebuie sa fie de tipul DataInputStream, pentru a putea folosi metode ca 'readLine' si altele. Si familia claselor Reader-Writer foloseste clase decorator:
public abstract class FilterReader extends Reader
public int read() throws IOException
public int read(char cbuf[ ], int off, int len) throws IOException
public void close() throws IOException
}
Clasele PrintStream si PrintWriter
adauga claselor filtru metode pentru scriere cu format (cu conversie) intr-un
flux de iesire, metode cu numele "print" sau "println".
Clasa BufferedReader adauga clasei Reader o metoda "readLine" pentru a permite citirea de linii din fisiere text.
Clasele InputStreamReader si OutputStreamWriter sunt clase adaptor intre clasele "Stream" si clasele "Reader-Writer". Clasele adaptor extind o clasa Reader (Writer) si contin o variabila de tip InputStream (OutputStream); o parte din operatiile impuse de superclasa sunt realizate prin apelarea operatiilor pentru variabila flux (prin "delegare"). Este deci un alt caz de combinare intre extindere si agregare.
Un obiect InputStreamReader poate fi folosit la fel ca un obiect Reader pentru citirea de caractere, dar in interior se citesc octeti si se convertesc octeti la caractere, folosind metode de conversie ale unei clase convertor. Codul urmator ilustreaza esenta clasei adaptor, dar sursa clasei prevede posibilitatea ca un caracter sa fie format din doi sau mai multi octeti (conversia se face pe un bloc de octeti):
public class InputStreamReader extends Reader
public InputStreamReader(InputStream in)
public int read() throws IOException
. . . // alte metode
}
Exemplu de utilizare a unei clase adaptor pentru citire de linii de la tastatura :
public static void main (String arg[ ]) throws IOException
}
Politica de confidentialitate | Termeni si conditii de utilizare |
Vizualizari: 1920
Importanta:
Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved