Urăsc scalabilitatea.
Nu s-ar putea spune despre mine că sunt un tip care se aprinde prea repede. Ba chiar nevastă-mea îmi repetă în fiecare dimineaţă că sunt prea moale.
Nu s-ar putea spune despre mine că port ranchină.
Sau că aş fi intolerant.
Şi cu toate astea, urăsc teribil scalabilitatea.
Scalabilitatea defineşte modul în care se comportă un sistem odată cu creşterea numărului de utilizatori – mai precis, exprimă capacitatea sistemului de a acomoda mai mulţi utilizatori prin simpla creştere a numărului de servere.
Şi ce-i cu asta?
Ei bine, ani de zile am ascultat cum pentru a creşte scalabilitatea, trebuie să folosesc OOP şi arhitecturi n-tier. Vreau ca sistemul meu să scaleze bine? Nimic mai simplu – separ logica programului de interfaţa cu utilizatorul şi de codul de acces la baza de date. Pun interfaţa grafică pe calculatoarele utilizatorilor, logica programului pe un server de aplicaţie dedicat şi bazele de date pe un server SQL. Am nevoie să suport încă 500 de utilizatori? Nimic mai simplu – copiez logica programului pe încă n servere de aplicaţie care vor accesa în continuare acelaşi server SQL. Se cheamă ca am “scalat” sistemul.
Aşadar scalabilitatea înseamnă arhitecturi n-tier.
Ei bine ... nici unul dintre clienţii mei nu are mai mult de 100 de computere în firmă. Cu atât mai puţin 100 de utilizatori concurenţi. E drept, scalabilitatea poate fi o chestiune importantă pentru anumite proiecte (şi anumiţi programatori). Scalabilitatea însă îşi spune cuvântul în faţa a sute de useri concurenţi care exploatează acelaşi sistem. Aplicaţiile mele Windows Forms sunt aplicaţii de business, folosite doar de către salariţii autorizaţi în intranet-ul clientului. Şi cum n-am de unde să adun atâţia useri – consider că scalabilitatea nu e o necesitate pentru mine. Aplicaţiile mele n-au apucat (încă?) să sufere prea tare de probleme de performanţă - clienţii mei însă, ţin să-mi ceară zilnic funcţii noi sau adaptarea aplicaţiilor existente mai aproape de necesităţile lor de business.
Clienţii mei nu au ferme de calculatoare - aşa că pot considera scalabilitatea nerelevantă pentru mine – şi pot renunţa la arhitecturile n-tier sofisticate, la separarea logicii de interfaţa cu utilizatorul şi de codul de conectare la baza de date. Îmi pot permite un cod mai compact – pot face Rapid Application Development – asta însemnă că pot să-mi iau mai repede banii J .
Ei bine, mă apropii de motivul pentru care urăsc scalabilitatea. Am citit de prea multe ori că arhitecturile n-tier promovează scalabilitatea – de suficient de multe ori încât, în subconştient, am ajuns să identific n-tier-ul cu scalabilitatea. Şi am demarat câteva proiecte ignorând cu bună ştiinţă o arhitectură sănătoasă considerând nu mi-ar putea aduce -decât- scalabilitate.
Cert e că n-am avut noroc – Rapid Application Developement-ul şi tehnicile de programare “drag and drop” s-au întors repede împotriva mea. Prioritaţile s-au schimbat din “a livra cât mai repede un proiect nou către un client nou” – în “a întreţine aplicaţiile existente şi a le adăuga funcţii la cererea clienţilor existenţi”. A trebuit să revin asupra arhitecturii sistemului şi, încet încet, constrâns de presiuni imediate, să reinventez roata. M-am întors spăşit către OOP şi n-tier – nu pentru că mi-ar fi oferit scalabilitatea mult trâmbiţată sau eficienţă sporită in dezvoltare - nici pentru că ar fi la modă sau pentru că “aşa se face la casele mai mari” – ci pentru că în direcţia asta am fost împins de necesităţile proiectelor mele.
Acest articol se vrea "episodul pilot" dintr-un serial mai lung, despre aventurile şi experimentele mele şi ale colegului meu in .Net. Ca în orice serial - o să aveţi parte de momente mai înteresante şi de clipe de somnolenţă: vor fi episoade despre utilizarea claselor proprii în locul TypedDataSet-urilor, despre suportul pentru valori DBNull (NullableTypes), despre DataBinding-ul pe obiecte (în contrast cu binding-ul pe DataSet-uri), despre regulile noastre de design a unei interfeţe grafice Windows Forms, despre generarea de cod cu CodeSmith, despre ORM si DataLayere, despre arhitectura unui Business Layer modular... Nu vă entuziasmaţi - nu sunt soluţii garantate, gata de a fi incluse în aplicaţiile voastre - nu e un framework care să vă rezolve toate problemele - e doar povestea unor programatori cu şefi cumsecade şi prea mult timp liber la dispoziţie.
Regulile de validare.
O bună parte din “logica” aplicaţilor noastre se traduce în reguli de validare. Din păcate, validările înseamnă în cele mai multe cazuri cod suficient de monoton şi neinteresant, cu care nu prea îmi vine să-mi încep ziua... Aşs că de multe ori nu am acordat suficientă atenţie validărilor.
Alteori, validările sunt chestiuni “de bun simţ” – atât de evidente, încât pare inutilă verificarea unor asemenea condiţii ( cine s-ar fi găndit să valideze într-o aplicaţie de salarii că “salariul de angajare trebuie să fie musai mai mare decât zero”?).
Din păcate însă, utilizatorii nu duc niciodată lipsă de imaginaţie.
Şi e de-a dreptul bizar modul în care o întreagă aplicaţie de salarii poate să ruleze, fără să ridice nici o excepţie, calculând dările către stat pentru un salariu negativ. Şi la fel de bizar este şi telefonul operatorului de la bancă care iţi spune că sistemul lor îţi refuză în intregime viramentul salariilor pe carduri pentru că nu reuşeşte să plătească o sumă negativă... Heh - urmează să explici cum stă treaba cu validările celor o mie de salariaţi indignaţi că nu şi-au primit salariile pe data de 31...
Din fericire, majoritatea condiţiilor de validare (de genul salariul brut > 0) sunt relativ uşor de exprimat. Mi-am pus la punct destul de repede un user control derivat din TextBox (l-am botezat NumericBox) – care permite doar introducerea cifrelor şi verifică dacă valoarea introdusă se încadrează între două proprietăţi (Minim şi Maxim). Au urmat controale de validare a textelor cu regular expressions (RegExBox), de introducere a datelor calendaristice (DateBox), ba chiar de introducere a intervalelor de timp (la care – deh - e musai ca DataFinală să fie ulterioară DateiIniţiale!).
Unele din aceste controale conţin cod trivial : spre exemplu, NameBox-urile – care sunt folosite pentru a introduce numele, prenumele, funcţia sau compartimntul – nu sunt alteceva decât nişte simple TextBox-uri care au grijă să înlăture automat spaţiile (trim) şi să transforme textul introdus în majusculă iniţială. (popescu ION -> Popescu Ion).
Alte controale însă înglobează cod mai puţin trivial ; spre exemplu, controlul de editare al codului numeric personal (codul de 13 cifre, din buletin). CNPBox-ul este derivat dintr-un NumericBox (pentru a permite doar introducerea cifrelor), însă are o condiţie caracteristică mai curând textelor (lungimea de exact 13 caractere); primul caracter poate fi doar 1,2,5 sau 6, urmatoarele 6 caractere trebuie să reprezinte o dată validă (data naşterii in format an luna zi) şi, cel mai important, ultima cifră este o cifră de control care trebuie verificată cu formula
….
int[] coeficienti = new int[] {2,7,9,1,4,6,3,5,8,2,7,9};
int sumaControl = 0;
for (int i=0;i<12;i++)
sumaControl += int.Parse(_cnp.Substring(i,1) ) * coeficienti[i];
int cifraControl = sumaControl % 11 ;
if (cifraControl == 10)
cifraControl = 1;
if (cifraControl != int.Parse(_cnp.Substring(12,1) ) )
throw new ValidationException("Codul numeric personal nu este corect !");
...
Colecţia mea de controale cu “auto-validare” a crescut destul de repede şi, pentru o vreme, m-a facut destul de fericit. Îmi era destul de uşor să pun repede pe picioare un ecran de introducere de date destul de bine validat – tot ce trebuia făcut era să aleg controalele potrivite din ToolBox, să configurez corect câteva proprietăţi (gen Minim si Maxim, sau un regular expression) şi eventual să mai scriu câte un rând-două pe evenimentul de Validate pentru a acoperi cazurile cu adevărat particulare. Separarea pe componente şi construirea formurilor prin “asamblare” este într-adevăr destul de eficientă – din păcate însă, nu e teribil de explicită; revenind dupa vreo lună asupra unui asemenea form, îmi era uneori destul de greu să reconstitui toate regulile de validare care se aplicau asupra unor date introduse de către utilizator. Trebuia să iau în considerare toate regulile definite implicit de către controlul de editare, să intuiesc influenţa valorilor puse în proprietăţile de tip Minim/Maxim şi – nu în ultimul rând – să verific existenţa codului special din evenimentul de Validate. Exprimarea regulilor de validare în atâtea forme diferite îţi poate crea dificultăţi cănd trebuie să asiguri consistenţă între mai multe formuri de editare.
Lucrurile se complică mult mai tare în cazul ecranelor master-details. Spre exemplu, salariatul poate avea mai mulţi copii în întreţinere (pentru a beneficia în felul ăsta de oarece deduceri din impozit). Pentru fiecare copil trebuie introdus codul numeric personal – şi validarea CNP-ului în cazul ăsta e destul de importantă, pentru că CNP-urile incorecte sunt refuzate de aplicaţiile de la administraţia financiară. În plus, pentru fiecare copil trebuie introdusă (şi validată) perioada în care a fost în întreţinere (DataIniţială şi DataFinală).
Concluzia? Avem nevoie de validări la nivelul detaliilor în aceeaşi măsură ca şi la nivelul înregistrării “master”. Pentru implementarea regulilor de validare la nivelul detaliilor suntem constrânşi deci să folosim aceleaşi controale de validare (ne trebuie musai acel CNPBox … ). Prin urmare, avem nevoie de un grid care să accepte să editeze datele suprapunând controalele noastre custom (ba chiar să suporte controale multi-coloană, precum controlul de editare al unui interval de timp). Cerinţele astea sunt suficient de exigente încât să scoată din discuţie o bună parte din gridurile de pe piaţă. E drept, există şi câteva modalităţi de a ocoli gridurile - spre exemplu, putem folosi o listă read-only pentru a alege rândul curent urmând ca editarea să fie făcută intr-un form separat ce se deschide cu dublu-click sau într-un grup de controale poziţionate undeva sub lista de selecţie. Sau am putea înlocui gridurile cu “repeatere” – containere care să repete controale de editare pentru fiecare rând din tabelul ce trebuie editat. Toate aceste soluţii însă pierd ceva din ergonomia şi simplitatea unui grid şi – trebuie să recunoaştem - ridică oarece întrebări asupra modului în care se poate face editarea şi validarea detaliilor.
Calcule.
La o privire mai atentă, cazurile documentelor master-details se dovedesc destul de frecvente - probabil cel mai comun exemplu fiind editarea unei facturi. O factură e un document tipic master/details - cu un antet (ce conţine Data, NumarDocument, Client, Valoare Factură, TVA şi ValoareTotală) şi unul sau mai multe rânduri (având o denumire de produs, unitate de măsură, cantitate, preţ, valoare, TVA şi totalul aferent rândului).
Unul dintre cele mai importante lucruri în scenariile master/details este asigurarea integrităţii şi consistenţei datelor între antet şi detalii; în cazul facturii, va trebui să avem grijă ca totalulurile de factură salvate în baza de date să fie întotdeauna egale cu suma detaliilor. Soluţia evidentă – vom salva antetul şi detaliile în cadrul unei tranzacţii.
Există însă un amănunt important de care va trebui să ţinem cont ori de câte ori lucrăm cu tranzacţii: tranzacţiile se bazează pe lock-uri – iar lock-urile pot produce efecte neplăcute dacă sunt folosite incorect. Pentru a reduce riscul conflictelor, al timpilor de aşteptare pentru eliberarea unui lock sau al dead-lock-urilor, e foarte important ca o tranzacţie să dureze căt mai puţin. Sub nici o formă nu ne putem permite să deschidem o tranzacţie în clipa în care utilizatorul începe editarea facturii, să o ţinem deschisă pe toată durata completării datelor şi să o comitem abia la apăsarea butonului de Save. Ca orice acţiune a userului, editarea unei facturi ar putea dura oricât între 30 de secunde şi 30 de minute. Asta dacă nu luăm în calcul cazul utilizatorilor care îşi lasă seara programele deschise pentru a ştii ce au de făcut a doua zi de dimineaţă...
Soluţia e simplă – vom face editarea facturii în memorie, urmând să deschidem tranzacţia abia în clipa apăsării butonului de Save. În acest scop vom folosi un Dataset (sau mai precis un TypedDataset) în care vom defini cele două tabele – antetul şi detaliile. (Posibilitatea definirii unor documente multi-tabelă e unul din lucrurile care mi-au plăcut cel mai tare la primul contact cu Ado.Net – şi în continuare găsesc mult mai potrivit genul ăsta de utilizare a DataSet-urilor decât încercările de a aduce întreaga bază de date în memorie in DataSet-uri cu zeci de tabele şi mii de rânduri).
Pentru editarea Datei Facturii, Numarului şi Clientului vom folosi câte un DateBox, un NumericBox şi un ComboBox legate prin DataBinding de tabela Antet din DataSet. Pentru rândurile facturii însă, aşa cum am amintit şi mai sus – există mai multe variante: putem folosi un DataGrid editabil, un repeater sau o listă read-only sincronizată cu un form (sau un grup de controale) pentru editarea rândului curent.
Indiferent de varianta eleasă – avem de implementat câteva calcule simple: la orice modificare a cantităţii sau a preţului, va trebui să recalculăm şi să afişăm valoarea, tva-ul şi totalul răndului curent şi să actualizăm totalurile facturii. Şi pentru acest lucru există câteva variante, destul de diferite ca abordare:
· Putem să ne folosim de evenimentele Validate ale controalelor de editare pentru a iniţia recalcularea. Din păcate, pentru cazul în care facem editarea detaliilor cu un grid standard, nu avem la dispoziţie decât evenimentul CellChange – care e destul de limitat. Pe de altă parte, controlul de editare din repeater şi form-ul separat de editare au acces direct doar la rândul curent şi vor avea mici probleme încercând să actualizeze totalurile facturii.
· Putem folosi coloane auto-calculate – completând proprietatea Expression a Valorii, TVA-ului şi Totalului în designer-ul TypedDataset-ului. Deşi la prima vedere foarte elegantă, această metodă are limitările ei: editorul de formule nu este prea comod, expresiile pot fi verificate doar la run-time, procesul de debug e mai dificil, se complică puţin încărcarea şi salvarea dataset-ului în baza de date – şi, nu în ultimul rănd, datorită lipsei unor CallBack-uri, suntem limitaţi la posibilităţile de exprimare ale expresiilor. If-urile şi DBNull-urile au darul de a complica destul de tare expresiile – imaginaţi-vă însă că ar trebui să putem implementa şi discount-uri a căror valoare variază în funcţie de cantitate...
· Putem folosi evenimentele de ColumnChanged sau RowChanged de la nivelul tabelei de detalii din DataSet. Chiar dacă evenimentele tabelelor nu sunt direct accesibile din property grid (ca evenimentele controalelor), e foarte simplu să definim un handler al acestor evenimente direct din cod :
this.factura1.Detalii.RowChanged+=new DataRowChangeEventHandler(Detalii_RowChanged);
this.factura1.Detalii.ColumnChanged+=new DataColumnChangeEventHandler(Detalii_ColumnChanged);
În plus, în cazul folosirii acestor evenimente căpătăm independenţă faţă de interfaţa cu utilizatorul. Indiferent dacă folosim un super-grid, un repeater sau un form separat de editare, calculele funcţionează în acelaşi mod. Nu mai suntem constrânşi să folosim un anumit grid comercial sau să explicăm userilor de ce e mai bine să editeze fiecare rănd într-o fereastră dialog deschisă cu dublu click. În plus, logica de calcul poate fi oricât de complicată (nu mai suntem limitaţi de expresii).
Folosirea evenimentelor DataSet-urilor va fi mult încurajată şi de Partial Classes din Visual Studio 2005; cu partial classes, vom putea scrie codul asociat evenimentelor într-un fişier separat de fişierul TypedDataset-ului generat de IDE – ambele fişiere însă vor fi compilate împreună în aceaşi clasă. TypedDataset-ul, având acum codul de calcul asociat, poate fi folosit în mai multe formuri (am putea defini spre exemplu un wizard simplificat de emitere a facturilor şi un ecran avansat care să permită editarea facturilor cu discount – ambele bazate pe acelaşi dataset care implementează singur calculele).
Chiar dacă în Visual Studio 2003 nu putem folosi partial classes, avem totuşi o modalitate de a lega codul de calcul de un TypedDataset: putem deriva din TypedDataSet. De fapt, va trebui să derivăm şi din FacturaDataSet şi din DetaliiDataTable şi din DetaliiDataRow. Ce-i drept, un pic cam mult cod pentru a face calculul unei facturi. Ideea însă poate fi tentantă, pentru că am putea implementa şi codul de validare tot la nivelul DataSet-ului. Vă mai aduceţi aminte? Aveam ceva probleme cu validarea detaliilor la documentele master/details; în exemplul de mai sus, întrucât trebuia să validăm codul numeric personal al copiilor unui salariat, eram obligaţi să folosim CNPBox-ul în locul unui grid standard – pentru că doar CNPBox-ul ştia să valideze CNP-urile.
Am putea însă muta întreg codul de validare de la nivelul controalelor de editare în nişte funcţii dintr-o bibliotecă de validare. Putem apela apoi aceste funcţii din evenimentele DataTable-ului pentru a asigura validarea datelor din detalii. Asta ne-ar putea aduce independenţă totală de controalele de editare – indiferent de soluţia de editare aleasă, vom avea şi calcule şi validări.
Şi totuşi ...
Tocmai am ajuns la concluzia că ne-ar prinde foarte bine un TypedDataSet care să aibă incorporat cod de calcul şi de validare. Avem deci nevoie de date împachetate la un loc cu cod de manipulare al acelor date. Aşa este. Date + cod = clase. Am ajuns la definiţia claselor potrivit OOP. De remarcat însă - clasele permit scenarii mult mai elegante de acomodare a codului decât ne pot oferi evenimentele DataSet-ului. Am putea scrie codul necesar pe property set-uri – nu am mai fi obligaţi să scriem codul de calcul şi de validare al tuturor coloanelor unui tabel înghesuit într-un singur handler de eveniment ColumnChanged.
Oare ar trebui să înlocuim TypedDataSet-ul cu o clasă definită de noi în întregime?
Mă gândesc că aş putea scrie o bucată de cod pentru detaliul de factură care să arate cam aşa :
public class RandFactura : DetaliuFacturaBase
{
private decimal _cantitate;
public decimal Cantitate
{
get
{
return _cantitate;
}
set
{
_cantitate = value;
}
}
private decimal _pret;
public decimal Pret
{
get
{
return _pret;
}
set
{
_pret = value;
}
}
public override decimal Valoare
{
get
{
return this.Cantitate * this.Pret;
}
}
public override decimal Tva
{
get
{
return this.Valoare * this.ProcentTva;
}
}
public override decimal Total
{
get
{
return this.Valoare + this.Tva;
}
}
}
public class AntetFactura : AntetFacturaBase
{
public override decimal Valoare
{
get
{
decimal rez=0;
foreach (RandFactura randCurent in this.Randuri)
rez += randCurent.Valoare;
return rez;
}
}
public override decimal Tva
{
get
{
decimal rez=0;
foreach (RandFactura randCurent in this.Randuri)
rez += randCurent.Tva;
return rez;
}
}
public override decimal Total
{
get
{
decimal rez=0;
foreach (RandFactura randCurent in this.Randuri)
rez += randCurent.Total;
return rez;
}
}
}
Am renunţat chiar şi la efectuarea calculelor la modificarea cantităţii sau a preţului – acum valoarea, tva-ul şi totalul sunt read-only, fiind calculate la accesare (query). Dat fiind că acest cod va rula în interfaţa cu utilizatorul nu e cazul să imi fac probleme de performanţă din cauza apelurilor repetate (e vorba de milisecunde aici) – prefer însă această abordare pentru că este mult mai simplă şi mai explicită - chestiune care poate să conteze în cazul unor calcule mai complicate. E o chestiune de gust – ceea ce contează însă e că ambele variante sunt suportate la fel de bine de clase definite de utilizator.
Un punct în plus pentru propriile noastre clase în faţa DataSet-urilor.
Ar trebui oare să renunţăm total la TypedDataSet-uri (prin urmare, la o bucată semnificativă din ADO.Net) pentru .. nişte clase user-defined ? Sună a blasfemie.
Revelaţia.
Sunt momente când mă simt norocos. Momente când o idee nouă capătă suficientă forţă încât imi demolează complet modul de gândire - îmi schimbă radical viziunea asupra unui domeniu.
Sunt momente când mă loveşte “Revelaţia”.
Pentru mulţi prima revelaţie vine odată cu abecedarul. Momentul când literele încetează să mai fie doar litere şi se unesc brusc în cuvinte şi propoziţii. Momentul când realizează că ştiu să citească.
Eu unul nu mai ţin minte absolut nimic de atunci.
Probabil totul a fost altfel pentru mine - cititul a venit greu, gradual, literă cu literă, cuvânt cu cuvânt. Suficient de încet încât să nu mă surprindă şi să se piardă în cotidian. Singurul lucru pe care mi-l aduc aminte din perioada aia e ziua când am învăţat să-mi cumpăr singur ştrudele cu mere de la patiseria din colţ. Aia da revelaţie !
Ca programator, prima revelaţie m-a lovit destul de târziu.
Învăţasem Basic-ul pe HC-uri, apoi am dat de PC-uri, de MS-DOS, de Pascal, C, C++ .. Heh .. am înţeles chiar şi pointerii !
Toate astea au venit treptat, fără să mă surprindă (nu fără entuziasmul de moment însă).
Mă chinuiam în Fox 2.6 ... plimbându-mă în sus şi-n jos prin câteva tabele, adunând şi scăzând, calculând algoritmic tot felul de rapoarte pentru o firmă de contabilitate cu 3 angajaţi.
Şi într-o zi am dat cu nasul de Select SQL prin help.
SQL-ul a fost prima mea revelaţie în programare.
Până atunci veneram ALGORITMUL. Algoritmul definea un proces pe care îl puteam urmări mental, pe care îl puteam calcula şi fără computer, cu pixul pe hârtie. Îmi trebuia o sumă de salarii? Trebuia să "instruiesc" calculatorul să parcurgă tabela salariaţilor rând cu rând, cumulând într-o variabilă salariul din rândul curent.
Atâţia ani am fost convins că programarea înseamnă traducerea cât mai exactă a unui proces mental într-un limbaj de programare - şi brusc , a venit SELECT Sum(Salariu) FROM Salariati. Nu mai trebuia să spun CUM. Era suficient să spun CE vreau. În plus, calculul mergea de câteva mii de ori mai repede ... La scară personală, şocul a fost probabil comparabil cu efectul teoriei Relativităţii pentru fizica începutului de secol 20. Mi-a demolat complet convigerile de până atunci despre programare - SQL-ul a detronat ALGORITMUL.
Suficient cât să devină o obsesie.
Au trecut ani buni şi încă mai sunt marcat de descoperirea SQL-ului. Fie că am lucrat în Access, Visual Basic sau C#, am gândit întotdeauna schema bazelor de date mai întâi. Ori de câte ori scriu prea mult cod o alarmă începe să urle în minte : “ai grijă, nu te întoarce la algoritm! Nu uita, pentru calcule există SQL! Orice ai face, nu vei putea bate SQL Server-ul!”.
Ei bine, răul ăsta de înălţime a fost primul lucru pe care l-am simţit cănd mi-am pus problema înlocuirii DataSet-urilor cu propriile mele clase. Folosirea claselor în locul DataSet-urilor începea să sune prea aproape de ceea ce credeam că înseamnă n-tier, de COM+ şi EnterpriseServices. Aveam în minte imaginea întregii baze de date încărcate în memorie sub formă unor colecţii de obiecte – vedeam bucle while care enumerau mii de obiecte salariat încercând să facă totaluri. Nici un fel de clasă nu m-ar putea convinge să mă întoarc din nou la Algoritm.
Apoi am realizat că şi DataSet-ul e tot o banală clasă. Şi că folosisem de ceva vreme destule clase în codul de acces la baza de date – fie că erau RecordSet-uri din Ado fie că erau DataReadere sau DataSet-uri în Ado.Net. Am lucrat atâta timp cu Ado.Net-ul (cu obiecte deci), fără să încerc să încarc în memorie toţi salariaţii şi să enumăr toate rândurile dintr-un tabel pentru a calcula o amărâtă de sumă. Pentru astfel de cazuri, am folosit întotdeauna interogările directe SQL. De ce înlocuirea DataSet-urilor cu propriile mele clase ar impune şi renunţarea la interogările SQL ? Am folosit întotdeauna câte o instanţă de DataSet pentru a putea edita un document (o factură spre exemplu) – aş putea folosi exact aceeaşi abordare şi cu propriile mele clase.
Nu e o problemă de politică, nu e un război sfânt între Ado.Net şi clasele mele. Înlocuirea DataSet-urilor nu ar trebui să impună modificări majore în arhitectura generală a aplicaţiei – ar trebui doar să ofere suport mai bun pentru construirea unor interfeţe cu utilizatorul. Ar trebui să ofere o soluţie elegantă pentru calculele şi validările facturilor mele.
Recapitulare
Ce ar putea aduce clasele noastre?
* Implementarea regulilor de validare cât mai aproape de date – obţine independenţă faţă de controalele de editare; vom putea valida la fel de uşor CNP-urile copiilor ca şi CNP-ul salariatului. Putem utiliza acelaşi document (factură spre exemplu) în mai multe formuri (sau clase) : putem implementa wizarduri sau ecrane detaliate de editare, sau putem scrie chiar cod de import al unor facturi din fişiere externe care să folosească aceleaşi obiecte factură, cu aceleaşi validări ca în cazul introducerii datelor de către utilizator. Faţă de DataSet-uri, regulile de validare stau mai natural în codul property set-urilor decât înghesuite în vreun handler de eveniment ColumnChanging – codul este mai “curat” şi mai uşor de testat.
* Modalităţi de calcul. Toate argumentele validărilor sunt valabile şi aici – în plus, clasele definite de noi aduc posibilităţi noi de exprimare precum calcule implementate prin query-uri, în proprietăţi read-only.
* Moştenire. Încă un termen banalizat de utilizare excesivă – moştenirea poate fi însă un intrument foarte util pentru implementarea documentelor noastre. Spre exemplu, clientul ne poate cere să implementăm pe lângă facturile de vânzare de produse şi facturi de prestări servicii pe bază de abonament (cum ar fi service-ul). Validările şi calculele sunt aproape identice în cezul facturilor de service cu cele din cazul facturilor de vânzare de mărfuri – va trebui însă să ţinem cont de perioada abonamentului şi de datele din contractul de service. Folosind TypedDataSet-uri, am crea structuri paralele de date pentru facturile de marfuri şi facturile de servicii şi ar trebui să folosim nişte construcţii arficiale pentru a limita repetarea codului de validare şi de calcul. Moştenirea vine ca o soluţie naturală în acest caz (din pacate genul ăsta de reutilizare a codului nu este aplicabilă şi la dataset-uri). Iar dacă facturile de prestări servicii pe bază de abonament nu par a fi un caz suficient de des întâlnit – gânditi-vă la facturile de cumpărare în comparaţie cu cele de vânzare; aproape toate aplicaţiile mele au de a face în aceeaşi măsură cu facturile de vânzare şi cu cele de cumpărare – iar structurile sunt aproape identice.
* Object composition. Într-un mod similaral cu felul în care am construit interfaţa cu utilizatorul pentru editarea salariatului din componente precum NameBox, DateBox şi CNPBox, putem modela documentele noastre folosind clase (tipuri) definite în prealabil. Definiţia DataSet-urilor acceptă doar tipuri de bază (precum int sau string) şi asta a determinat externalizarea codului de validare în controale. Propria noatră clasă salariat însă ar putea fi compusă din proprietăţi de tipul Name sau CodNumericPersonal – tipuri care implementează automat validarea datelor. Un alt exemplu – definirea unei adrese la nivelul salariatului. Dataset-urile ne constrâng să definim câmpurile de Localitate, Stradă, Număr, Bloc, Scară, Etaj în aceeaşi tabelă cu numele şi prenumele salariatului. O clasă Salariat ar putea să arate mai curând astfel :
public class Salariat
{
private Name _nume;
public string Nume
{
get{return _nume;} // atenţie, se o conversie implicită
set{_nume = value;}
}
private Name _prenume;
public string Prenume
{
get{return _prenume;}
set{_prenume = value;}
}
private CodNumericPersonal _cnp;
public string Cnp
{
get{return _cnp;} // atenţie, se o conversie implicită
set{_cnp = value;}
}
private Adresa _adresaSalariat;
public Adresa AdresaSalariat
{
get{return _adresaSalariat;}
set{_adresaSalariat = value;}
}
}
Salariatul defineşte deci o proprietate de tip Adresa. Tipul adresă este structurat în Localitate, Strada, numar, bloc, scara, etaj, apartament – însă oferă şi funcţii de manipulare a acestor componente – spre exemplu pune la dispoziţie o metodă de concatenare a tutuor acestor componente utilă la afişarea adresei nestructurate (ca string) :
public class Adresa
{
private string _localitate;
public string Localitate
{
get{return _localitate;}
set{_localitate = value;}
}
……
private string _apartament;
public string Apartament
{
get{return _apartament;}
set{_apartament = value;}
}
public override string ToString()
{
StringBuilder rez = new StringBuilder();
if (_localitate != null && _localitate.Length !=0 )
sb.Append(“ Localitatea “ + _localitate);
if (_strada != null && _strada.Length != 0)
sb.Append(“ strada “ + _strada);
…..
if (_etaj != null && etaj.Length != 0)
sb.Append(“ etajul “ + etah);
return sb.ToString();
}
}
Am rămas însă dator cu o explicaţie pentru proprietatea Cnp a salariatului din codul de mai sus : am expus numele şi Cnp-ul sub forma unor proprietăţi de tip string, chiar dacă variabila internă are tipul proprietar (Name şi respectiv CodNumericPersonal). Am folosit această tehnică pentru a putea avea o mai bună compatibilitate cu DataBinding-ul şi cu controalele de editare de tip Textbox, care nu suportă tipuri custom. Codul de mai sus funcţionează dacă în clasa CodNumericPersonal am definit operatori de cast implicit de la şi către string :
public class CodNumericPersonal
{
……
public static implicit operator String (CodNumericPersonal dinCod)
{
return dinCod.Text;
}
public static implicit operator CodNumericPersonal(string dinString)
{
return new CodNumericPersonal(dinString);
Î
}
Acest mic amânunt este foarte important pentru a putea edita fără probleme obiectul nostru cu orice fel de control de editare şi pentru a-l încărca cu date şi salva mai uşor în baza de date.
Mutarea codului de validare şi calcul din controale şi formuri în clase proprii, alături de date, oferă şi avantaje neaşteptate: codul devine mult mai uşor de testat – ba chiar se pretează la unit testing. Putem scrie mici bucăţi de cod care să verifice comportarea codului pentru doua trei scenarii tipice (spre exemplu, putem testa care este valoarea facturii în cazul în care nu există nici un rând sau daca sunt actualizate corect totalurile facturii în urma ştergerii unui rând).
Care sunt totuşi dezavantajele renunţării la DataSet-uri ?
· In primul rând, avem mult mai mult cod de scris. Probabil motivul principal pentru care DataSet-urile sunt atât de tentante este integrarea foarte bună cu IDE-ul: e suficient să facem drag and drop cu un tabel din baza de date în designerul TypedDataSet-urilor şi mediul va genera cod pentru tabelul respectiv. Pentru a nu ceda nervos de câte ori vom începe să scriem codul pentru un tabel nou, va trebui să găsim o modalitate de generare a codului propriilor noastre clase. Personal, m-am îndrăgostit de CodeSmith – un generator de cod free, bazat pe template-uri, având o sintaxă identică cu cea din Asp.Net, şi care se integrează şi el destul de bine în IDE. Există însă multe alte soluţii de generare sau modelare vizuală a codului.
· Suportul pentru valori DBNull. Dataset-urile oferă un suport minimal (aş spune eu puţin cam incomod) pentru valorile null din baza de date. Va trebui să analizăm în amănunt necesitatea DBNull-urilor (şi diferenţa între null şi DBNull). Ca posibile soluţii vom evalua System.Data.SqlTypes şi o bibliotecă open source numită NullableTypes. Aştept cu interes şi suportul pentru DBNull promis de .Net 2.0
· DataBinding-ul. Deşi databinding-ul windows forms în .Net 1.1 suportă binding şi pe alte clase decăt DataSet-urile, este nevoie de implementarea unor interfeţe specifice de suport a databinding-ului şi de căteva trucuri de evitare a unor bug-uri. Situaţia se pare că a fost luată în consideraţie mai atent în .Net 2.0 unde DataBinding-ul pe clase custom a fost reproiectat şi pare să fie mult mai bine pus la punct.
· DataAdapterele şi suportul pentru încărcarea şi salvarea în baza de date. DataSet-urile au un suport decent din partea infrastructurii Ado.Net pentru încărcarea din baza de date şi pentru salvarea datelor. Nici măcar în cazul DataSet-urilor nu sunt acoperite însă toate scenariile (spre exemplu, salvarea corectă a unui dataset master-details cu cheie primară de tip Identity se poate dovedi o adevărată aventură – vezi Q320301). Pentru a încărca însă obiectele noastre cu date va trebui să analizăm posibilităţile oferite de Ado.Net şi de către biblioteci ORM precum Nhibernate.
Aşadar, toate aceste neajunsuri îşi au soluţiile lor. Vom analiza în amănunt în episoadele viitoare fiecare din aceste probleme şi modalităţile de rezolvare disponibile.
Cert e că există motive suficiente pentru a decupla logica programului de formurile şi controalele de editare. Super-specializarea controalelor, deşi promovează RAD, îşi atinge repede limitările (aşa cum am văzut în cazul CNPBox-ului). Implementarea documentelor prin clase proprii, deşi necesită ceva mai mult efort iniţial, se dovedeşte a fi o soluţie flexibilă, capabilă să suporte într-un mod natural şi elegant mult mai multe scenarii decât poate acomoda un TypedDataset. În plus, vom câştiga claritate, mentenabilitate şi compatibilitate cu unit testing. Şi mai presus de toate, s-ar putea să câştigăm şi ceva … scalabilitate.