Acest capitol este din carte

Acest capitol este din carte

Acest capitol este din cartea 

Elementul 2: luați în considerare un constructor atunci când vă confruntați cu mulți parametri ai constructorului

Fabricile și constructorii statici împărtășesc o limitare: nu se ridică bine la un număr mare de parametri opționali. Luați în considerare cazul unei clase care reprezintă eticheta Fapte nutriționale care apare pe alimentele ambalate. Aceste etichete conțin câteva câmpuri obligatorii - dimensiunea porției, porții pe recipient și calorii pe porție - și peste douăzeci de câmpuri opționale - grăsimi totale, grăsimi saturate, grăsimi trans, colesterol, sodiu și așa mai departe. Majoritatea produselor au valori diferite de zero doar pentru câteva dintre aceste câmpuri opționale.

când

Ce fel de constructori sau fabrici statice ar trebui să scrieți pentru o astfel de clasă? În mod tradițional, programatorii au folosit modelul constructor telescopic, în care furnizați un constructor cu numai parametrii necesari, un altul cu un singur parametru opțional, un al treilea cu doi parametri opționali și așa mai departe, culminând cu un constructor cu toți parametrii opționali. Iată cum arată în practică. Din motive de scurtă durată, sunt afișate doar patru câmpuri opționale:

Când doriți să creați o instanță, utilizați constructorul cu cea mai scurtă listă de parametri care conține toți parametrii pe care doriți să îi setați:

În mod obișnuit, această invocare a constructorului va necesita mulți parametri pe care nu doriți să îi setați, dar oricum sunteți forțați să le transmiteți o valoare. În acest caz, am trecut o valoare 0 pentru grăsime. Cu „doar” șase parametri, acest lucru poate să nu pară atât de rău, dar scapă rapid din mână pe măsură ce numărul parametrilor crește.

În scurt, modelul constructor telescopic funcționează, dar este greu să scrieți codul clientului atunci când există mulți parametri și este mai greu să-l citiți. Cititorul se lasă întrebat ce înseamnă toate aceste valori și trebuie să numere cu atenție parametrii pentru a afla. Secvențele lungi de parametri tipări identici pot provoca erori subtile. Dacă clientul inversează accidental doi astfel de parametri, compilatorul nu se va plânge, dar programul se va comporta greșit în timpul rulării (articolul 40).

O a doua alternativă când vă confruntați cu mulți parametri de constructor este modelul JavaBeans, în care apelați un constructor fără parametri pentru a crea obiectul și apoi apelați metodele setter pentru a seta fiecare parametru necesar și fiecare parametru opțional de interes:

Acest model nu are niciunul dintre dezavantajele modelului constructor telescopic. Este ușor, dacă este puțin cam vorbitor, să creați instanțe și să citiți ușor codul rezultat:

Din păcate, modelul JavaBeans are dezavantaje serioase. Deoarece construcția este împărțită pe mai multe apeluri, un JavaBean poate fi într-o stare incoerentă parțial prin construcția sa. Clasa nu are opțiunea de a impune consistența doar prin verificarea validității parametrilor constructorului. Încercarea de a utiliza un obiect atunci când este într-o stare inconsistentă poate provoca eșecuri care sunt departe de codul care conține eroarea, deci dificil de depanat. Un dezavantaj conex este că modelul JavaBeans exclude posibilitatea de a face o clasă imuabilă (Punctul 15) și necesită un efort suplimentar din partea programatorului pentru a asigura siguranța firului.

Este posibil să se reducă aceste dezavantaje prin „înghețarea” manuală a obiectului atunci când construcția sa este completă și nu se permite utilizarea acestuia până când este înghețat, dar această variantă este dificilă și rareori folosită în practică. Mai mult, poate provoca erori în timp de execuție, deoarece compilatorul nu se poate asigura că programatorul apelează metoda de înghețare a unui obiect înainte de ao utiliza.

Din fericire, există o a treia alternativă care combină siguranța modelului constructor telescopic cu lizibilitatea modelului JavaBeans. Este o formă a modelului Builder [Gamma95, p. 97]. În loc să facă obiectul dorit direct, clientul apelează un constructor (sau fabrică statică) cu toți parametrii necesari și obține un obiect constructor. Apoi clientul apelează metode de tip setter pe obiectul constructor pentru a seta fiecare parametru opțional de interes. În cele din urmă, clientul apelează o metodă de construire fără parametri pentru a genera obiectul, care este imuabil. Constructorul este o clasă de membru static (articolul 22) al clasei pe care o construiește. Iată cum arată în practică:

Rețineți că NutritionFacts este imuabil și că toate valorile implicite ale parametrilor sunt într-o singură locație. Metodele de setare ale constructorului returnează constructorul în sine, astfel încât invocațiile să poată fi înlănțuite. Iată cum arată codul clientului:

Acest cod client este ușor de scris și, mai important, de citit. Modelul Builder simulează parametrii opționali numiți așa cum se găsește în Ada și Python.

La fel ca un constructor, un constructor poate impune invarianți parametrilor săi. Metoda de construire poate verifica acești invarianți. Este esențial ca acestea să fie verificate după copierea parametrilor de la constructor la obiect și că acestea sunt verificate pe câmpurile obiectului, mai degrabă decât pe câmpurile constructorului (articolul 39). Dacă orice invarianți sunt încălcați, metoda de construire ar trebui să arunce o IllegalStateException (Item 60). Metoda de detaliere a excepției ar trebui să indice care invariant este încălcat (articolul 63).

O altă modalitate de a impune invarianți care implică mai mulți parametri este aceea ca metodele setter să ia grupuri întregi de parametri pe care unii invarianți trebuie să le dețină. Dacă invariantul nu este satisfăcut, metoda setter lansează o excepție IllegalArgumentException. Acest lucru are avantajul de a detecta eșecul invariant de îndată ce parametrii invalizi sunt trecuți, în loc să așteptați să fie invocată versiunea.

Un avantaj minor al constructorilor față de constructori este că constructorii pot avea mai mulți parametri varargs. Constructorii, la fel ca metodele, pot avea un singur parametru varargs. Deoarece constructorii folosesc metode separate pentru a seta fiecare parametru, pot avea cât mai mulți parametri varargs doriți, până la unul pentru fiecare metodă setter.

Modelul Builder este flexibil. Un singur constructor poate fi folosit pentru a construi mai multe obiecte. Parametrii constructorului pot fi modificați între creațiile de obiecte pentru a varia obiectele. Constructorul poate completa unele câmpuri automat, cum ar fi un număr de serie care crește automat de fiecare dată când este creat un obiect.

Un constructor ai cărui parametri au fost setați face o fabrică abstractă fină [Gamma95, p. 87]. Cu alte cuvinte, un client poate transmite un astfel de constructor unei metode pentru a permite metodei să creeze unul sau mai multe obiecte pentru client. Pentru a activa această utilizare, aveți nevoie de un tip pentru a reprezenta constructorul. Dacă utilizați versiunea 1.5 sau o versiune ulterioară, un singur tip generic (articolul 26) este suficient pentru toți constructorii, indiferent de tipul de obiect pe care îl construiesc:

Rețineți că clasa noastră NutritionFacts.Builder ar putea fi declarată că implementează Builder .

Metodele care iau o instanță Builder ar constrânge în mod obișnuit parametrul de tip al constructorului utilizând un tip comodin delimitat (articolul 28). De exemplu, aici este o metodă care construiește un copac folosind o instanță Builder furnizată de client pentru a construi fiecare nod:

Implementarea tradițională Abstract Factory în Java a fost obiectul clasei, metoda newInstance jucând rolul metodei build. Această utilizare este plină de probleme. Metoda newInstance încearcă întotdeauna să invoce constructorul fără parametri al clasei, care poate nici nu există. Nu primiți o eroare în timpul compilării dacă clasa nu are un constructor accesibil fără parametri. În schimb, codul clientului trebuie să facă față InstantiationException sau IllegalAccessException în timpul rulării, ceea ce este urât și incomod. De asemenea, metoda newInstance propagă orice excepție aruncată de constructorul fără parametri, chiar dacă newInstance nu are clauzele throws corespunzătoare. Cu alte cuvinte, Class.newInstance întrerupe verificarea excepțiilor în timpul compilării. Interfața Builder, prezentată mai sus, corectează aceste deficiențe.

Modelul Builder are dezavantaje proprii. Pentru a crea un obiect, trebuie mai întâi să-i creați constructorul. Deși este puțin probabil ca costul creării constructorului să fie vizibil în practică, ar putea fi o problemă în unele situații critice de performanță. De asemenea, modelul Builder este mai detaliat decât modelul constructor telescopic, deci ar trebui utilizat numai dacă există suficienți parametri, să zicem, patru sau mai mulți. Rețineți însă că poate doriți să adăugați parametri în viitor. Dacă începeți cu constructori sau fabrici statice și adăugați un constructor atunci când clasa evoluează până la punctul în care numărul parametrilor începe să scape de sub control, constructorii învechi sau fabricile statice vor ieși ca un deget mare. Prin urmare, este de multe ori mai bine să începeți cu un constructor în primul rând.

În concluzie, modelul Builder este o alegere bună atunci când proiectăm clase ale căror constructori sau fabrici statice ar avea mai mult de o mână de parametri, mai ales dacă majoritatea acestor parametri sunt opționali. Codul clientului este mult mai ușor de citit și scris cu constructorii decât cu modelul tradițional de constructor telescopic, iar constructorii sunt mult mai siguri decât JavaBeans.