Sunday, February 19, 2006

Agile vs waterfall

Tocmai am citit proposalul (cuprinsul) noii carti a lui Dan Bunea - despre teoria si practica metodologilor agile.

Arata foarte bine.


Am surprins insa o potentiala capcana in modul in care sunt introduse metodologiile agile ca raspuns la esecul metodologiilor clasice.
Atentie la sindromul "shfortza mea e mai mare decat a ta" - C-ul e mai tare decat Basic-ul, .Net-ul e mai tare decat Visual Foxul si agile-ul e mai cool decat waterfall-ul.
Programatorii sunt fiinte teribil de sensibile si mai ales orgolioase - si argumentatiile de genul x e mai tare decat y au tendinta de a starni furtuni pentru ca sunt teribil de interpretabile in lipsa descrierii exacte a contextului ... (x e mai bun decat y pentru (si numai pentru) problema z).
Mare grija asadar la modul in care trebuie introdus/motivat agile-ul. - nu cred ca e musai o alternativa la "esecul" metodologiilor clasice.
N-as incerca sa-l vand ca pe un panaceu (universal), nici macar ca pe cura care te vindeca de waterfall.
Dan Blendea mi-a atras atentia asupra unui articol mai vechi al lui Joel - http://www.joelonsoftware.com/articles/FiveWorlds.html . Exista zone din industria de software unde nu se pot aplica tehnicile incrementale - si unde first shot is the only shot : embedded, game developement, drivers, poate chiar si aplicatiile cu foarte multi clienti.
Mai mult, waterfall-ul poate fi o motodologie de succes daca toti oamenii implicati lucreaza precum ceasornicul elvetian (inclusiv clientul care iti defineste corect si complet problema inca de la inceput). La drept vorbind exista foarte multe exemple de proiecte mari de succes scrise pe waterfall (nu stiu cate exemple de agile - adevarul e ca noi ne bucuram cand auzim ca M$-ul spre exemplu foloseste elemente agile, insa standardul in industria software cred ce e departe de a fi agile-ul).
Waterfall pleaca de la premiza ca orice schimbare costa - si costa din ce in ce mai mult cu cat produsul se apropie de faza finala.
Conceptele din waterfall probabil sunt imprumutate din industria traditionala : in momentul in care te apuci sa construiesti fundatiile unei cladiri, planurile arhitecturale trebuie sa fie complete si 100% corecte ; nu iti mai poti permite sa schimbi structura cladirii dupa ce ai construit primul etaj (sau sa accepti cereri de modificare din partea clientului chiar inainte de inaugurare).
Traditional, schimbarile sunt IMPOSIBILE dupa trecerea la urmatoarea faza - si arhitectii si constructorii au invatat sa se descurce cu chstia asta si sa-si faca treaba bine - iar clientii au invatat sa respecte contractul si sa inghita in sec cand vad cladirea care parca nu seamana cu ce aveau ei in cap.
Si totusi cladiri se construiesc destul de multe - si as zice ca in general, sunt destul de putine refuzate ca avand prea multe bug-uri. Asta ma face sa cred ca si waterfall-ul ar trebui sa mearga ok in software - diferenta probabil e fie ca arhitectii software si programatorii lucreaza mai prost decat meseriasii cu mistria, fie ca dinamica (durata de viata si necesitatea schimbarii) in software e mult mai mare decat in constructii.
Concluzia - as zice ca metodologiile agile nu incearca sa repare problemele din waterfall, ci mai curand incearca sa beneficieze de avantajele pe care ti le ofera lucrul la o chestie atat de virtuala precum softul. In software NU E IMPOSIBIL sa schimbi cate ceva dupa ce ai trecut la o faza avansata de executie si, daca respectam cateva reguli de baza, poate sa nu fie nici chiar asa de costisitor precum pare. In software ne putem permite sa schimbam planurile cladirii dupa ce am construit deja primul nivel si ne putem permite sa fim mai flexibili in dialogul cu clientul, ne putem permite sa amanam deciziile pe cat mai tarziu, sa minimizam costurile cerute de executia ireprosabila a fiecarui pas inainte de construirea urmatorului.
Si in definitiv - uneori e foarte greu sa explici clientului de ce o modificare foarte tarzie in specificatii implica costuri prea mari de implementare sau determina rescrierea produsului. Clientii STIU ca facem software si nu cladiri - clientii CER modificari cat mai tarziu (si dupa darea in folosinta ... ). Chestie valabila cu atat mai mult cu cat clientul e mai afon in notiunile de project management in software.
Pana la urma agile-ul NU te invata cum sa scrii soft (poti sa o faci si cu waterfall) - ci cum sa fi mai putin rigid, mai adaptabil, mai eficient economic, mai aproape de client.
Mai aproape de succes.

Cosmin
On 2/18/06, Dan Bunea wrote:
---------- Forwarded message ----------From: Dan Bunea Date: Feb 18, 2006 4:06 PMSubject: Book reloaded - second attempt TOCHi,I have refined now the TOC, and I belive I have done a much better jobnow. I have attached it in a document, with estimated on pages and anexplanation of what each will contain, and in the end a list ofpractices from XP, SCRUM, Crystal and Lean that will be exposed ineach part, that is more like a todo/reminder list for me, but Iincluded it as I believe adds to the clarity of what's going to beincluded.I am trying to come up asap, (early next week) with some content. Someof the content will be adapted from my previous writing(communication, XP, TDD) so that we are still on schedule for thedelivery.Thank you,Dan Buneahttp://danbunea.blogspot.com--Dan Buneahttp://danbunea.blogspot.com

Saturday, January 07, 2006

Muzica buna si vinul vechi

Ati observat ca de la o anumita varsta, nu mai ascultam muzica cu acelasi entuziasm? Hm.. mi-aduc aminte de vremea cand eram in stare sa ascult 24 de ore The Wall fara sa simt nevoia sa deschid ochii. E drept, imi place la fel de mult si acum Pink Floyd. Insa nu-l mai pun in combina prea des.
De fapt, nu mai pun aproape nimic in combina.
Nu-i vorba ca nu-mi mai place muzica la fel de mult - insa parca nu mai simt imboldul de a sta si asculta un disc, la fel cum o faceam in liceu.
Daca stau putin si ma gandesc, alesul unui disc din stand si pusul pe platan e momentul dificil. Din clipa in care dau play, "the feeling begins" - magia revine cu aceeasi forta.
Asta e motivul pentru care ador vizitele prietenilor cu ureche muzicala. Imi ofera ocazia de a mai da o raita prin raftul cu bunatati, sa mai cautam o muzica veche.

Acelasi lucru se intampla cu programarea. Odata ce ne cresc burtile si nu ne mai regasim in pustii care clampaneau preocupati in tastaturi din pura pasiune, tindem sa privim la un proiect nou cu oarece ... inertie.

De aia trebuie sa se fi inventat pair programmingu'.

Cert e ca nici muzica, nici vinul nu par sa aiba aceeasi savoare de unul singur.
Guess what ?
La fel si cu butonatu'


....

PS: Tocmai am dat un anunt pe Ronua - daca nu-mi gasesc un partener de pair programming - poate macar gasesc un prieten cu care sa mai servesc din cand in cand "Selling England by the pound".

Sunday, July 31, 2005

TDD vs Unit Testing

Tocmai am terminat de citit azi noapte Test Driven Developement by example de Kent Beck .. O carte relativ interessanta (din pacate insa, un capitol destul de important e implementat in python, iar eu n-am avut rabdare sa invat python "on the fly" asa ca am cam sarit peste o mare parte a cartii). Chiar daca nu e atat de suculenta precum cartea lui de extreme programming, Test Driven Developement by example m-a pus putin pe ganduri. La un moment dat, kent arunca o vorba intr-o doara : "The problem with driving development with small-scale tests (I call them "unit tests," but they don't match the accepted definition of unit tests very well) is that you run the risk of implementing what you think users want, but having it turn out not to be what they wanted at all."Nu contextul ma intereseaza (acela ca fiecare clasa in sine poate sa functioneze perfect, insa aplicatia poate sa fie ratata - acelasi lucru il remarcase si Roy la TechEd) - cat ma intereseaza afirmatia ca testele scrise in TDD "nu prea sunt unit teste". Aparent, potrivit lui Kent Beck, exista o diferenta subtila intre testele din test driven developement si unit testing. Dar care ar fi asta? Kent nu pare sa revina la subiect iar eu nu pot sa fac decat speculatii.Asemanarile sunt clare - in forma - ambele genuri de teste arata la fel si sunt scrise folosind aceleasi framework-uri de testare (NUnit spre exemplu).Scopul lor insa pare diferit.Daca unit testing-ul isi propune un code coverage cat mai mare (optim 100%, dar sub 90% se considera ca este inutil), test driven developement nu incearca sa testeze codul, ci e doar un mod de a ghici interfetele potrivite pentru componentele pe care le lucrezi .. o alta modalitate de a face design .. design dupa miros (sau dupa ureche) daca vreti. Gata, ma pot relaxa, nu e nevoie sa ma mai simt vinovati ca n-am acoperit toate cazurile posibile atunci cand am incercat TDD.Perspectiva din care se sciu testele in cele doua metodologii este oarecum diferita. Unit testele, care vor acoperi toate code path-urile posibile, trebuie sa tina cont de implementarea interna a codului testat. Nu poti acoperi toate code path-urile daca nu stii foarte bine de ele. Unit Testele sunt o metoda de a asigura calitatea.Pe de alta parte, Test Driven Developement-ul pare sa priveasca problema mai curand din punctul de vedere al testarii componentei, a interfetei ei. Testele din TDD ar trebui sa ramana valabile (si neafectate) de schimbarile interne in componenta (refactorizarile). TDD-ul trebuie sa fie cat mai putin cuplat de codul pe care il testeaza.Care dintre practici e mai utila ? TDD sau Unit Testing-ul ? Luate ad-literam, nici una dintre ele nu este o metoda de a asigura 0% defects. De ce ?Daca urmaresti doar acoperirea mecanica a tuturor cailor prin cod (toate if-urile au fost calcate si pe ramura de then si pe ramura de else, toate buclele au fost rulate, etc), totusi Unit Testing-ul nu te poate scapa de erorile de logica introduse in formule. Spre exemplu, implementarea functiei factorialint Factorial(deNumar) {return 1;
poate fi corecta daca este testata cu valoarea 1, iar code coverage-ul va indica 100%. Insa implementarea e evident gresita. (Cazul e cam extrem, dar ideea ramane aceeasi : codul poate sa fie gresit dar greseala sa nu iasa in evidenta decat la anumiti parametrii).Altfel zis, 100% code coverage nu e o asigurare impotriva bug-ului .. In cel mai bun caz, te poate asigura ca nu-ti apar erori la run-time in cazul limbajelor (abordarilor) care nu sunt 100% type safe. Singura modalitate de a te asigura ca nu ai bug-uri este sa testezi 100% din code paths si 100% din parametrii posibili de apelare - ceea ce este absurd - pentru ca, spre exemplu, pentru functia factorial, ar insemna ca datele de verificare ale testului sa fie valorile precalculate ale tuturor factorialelor posibile (o infinitate).
Pe de alta parte, TDD-ul cred ca nu-si propune sa testeze codul sau sa izoleze bug-uri. Scopul principal al TDD este sa te indrume, pas cu pas, spre alegerea corecta a interfetelor, spre un design mai bun. E drept, avem nevoie de o acoperire cat mai justa cu teste (in sensul de Unit Teste) pentru a construi acea plasa de siguranta pentru clipele cand facem refactorizari. Si daca aceste teste nu acopera 100% din cazuri, la refactorizare apare riscul introducere unor bug-uri care sa nu fie depistate .. insa in aceeasi masura, apare si riscul corectarii unor bug-uri care nu erau depistate (la drept vorbind, refactorizarile ne duc la un cod mai simplu si mai clar, iar gandacii se feresc de locurile insorite - un cod mai simplu e mai putit susceptibil la bug-uri).

Wednesday, May 11, 2005

ClickOnce - o treabă făcută pe jumătate ?

ClickOnce versus Msi
În ultima vreme, am urmârit cu interes câteva prezentări despre noua tehnologie de deployment din .Net 2.0 – ClickOnce. Şi pentru prima dată, am senzaţia că am priceput cum stă treaba.
Aveţi ocazia să-mi dovediţi contrariul.

De ce ClickOnce ? Aveam MSI !
Întrebarea asta mă rodea de mai mult timp. Care e relaţia între ClickOnce şi Windows Installer (msi)? Va înlocui ClickOnce Msi-urile pentru aplicaţii .Net ?
MSI e o tehnologie foarte puternică şi matură. MSI poate face aproape orice şi-ar putea dori un developer de kit-uri – şi de cele mai multe ori, aşa cum bine a subliniat Ovidiu Platon, poate face mult mai mult decât se aşteaptă un programator sau un administrator.
Lucrez ca programator în cadrul departamentului de dezvoltare software al unei firme non-IT – clientul meu deci e chiar firma care mă plăteşte. Altfel zis - admin-ul clientului meu lucrează în celălalt birou şi il pot auzi cum trănteşte uşa ori de câte ori se enervează. Apropierea asta de admin m-a obligat să mă pun la curent cu modalităţile de deployment în Enterprise – nu de alta, dar adminul meu e prea urâcios ca să risc să-i spun că ar trebui să instaleze manual o dată la două săptămâni câte un update pe vreo 80 de computere...
Aşa am ajuns să apreciez combinaţia Active Directory/MSI advertising. O chestie super practică, uşor de pus pe picioare, dar din păcate prea puţin cunoscută. Ideea e următoarea : o aplicaţie care cere privilegii speciale la instalare poate fi instalată doar de pe un cont de administrator. Exemple sunt destule – orice aplicaţie care scrie în registry, care înregistrează obiecte COM, pune assembly-uri în GAC, creează o sursă de log in Event Log sau doar vrea să pună un icon pe desktop-ul din All Users.
Există însă o facilitate prea puţin cunoscută – un administrator poate doar să „facă reclamă” unei aplicaţii (advertise). Folosind “msiexec /jm nume.msi” de pe contul de admin, se crează doar iconiţele pe desktop sau în Start -> Programs – restul fişierelor nu sunt instalate. Dacă însă un user fără drepturi speciale va da dublu click pe iconiţa de “reclamă”, va determina pornirea instalării efective a aplicaţiei cu drepturi de admin. (Probabil ăsta e motivul pentru care msiexec este un serviciu – pentru ca procesul de instalare invocat de un user fără privilegii să poată fi executat sub local system account). Nu, nu e o breşă de securitate – pentru ca doar administratorul hotărăşte de la început ce aplicaţii vor fi “advertise” pe un calculator.
Cu toate că e cool, advertise-ul nu pare util în prea multe situaţii la prima vedere. Devine însă foarte interesant în combinaţie cu Group Policy Objects din Active Directory. Pe scurt, se poate crea o politică la nivelul unei întregi reţele care să specifice ce useri pot beneficia de o anumită aplicaţie. Indiferent de computerul pe care se logează, unui user i se vor crea automat, la log-on, prin advertise, iconiţele de care are nevoie pe desktop. Un dublu click pe o astfel de iconiţă determină instalarea imediată a aplicaţiei. Se pot face chiar şi update-uri de aplicaţii în acest mod – e suficient ca adminul să definească o nouă politică prin care să specifice un nou msi care va înlocui aplicaţia anterioară.
Aşadar existau, cel puţin la nivel de Enterprise, soluţii de deployment şi AutoUpdate destul de practice. De ce totuşi ClickOnce ?
Msi-ul e o soluţie super elaborată, creată ca răspuns la DLL hell. Ani de zile, programatorii au fost încurajaţi să dezvolte aplicaţii care îşi pun în comun dll-uri şi resurse; orice kit de instalare putea să aducă o versiune mai nouă sau mai veche al unui astfel de dll – iar Windows Installer-ul avea ca misiune principală gestionarea şi arbitrarea unor potenţiale conflicte între versiuni diferite ale aceluiaşi DLL partajat între aplicaţii.
.Net-ul vine însă cu o abordare revoluţionară: adio dll-uri partajate. Orice aplicaţie va funcţiona doar cu versiunea dll-ului cu care a fost compilată – motiv penstru care se încurajează ca o aplicaţie să conţină toate dll-urile necesare chiar în directorul aplicaţiei. Hard disk-urile sunt suficient de mari – nu putem considera că risipim spaţiu cu dll-uri redundante în fiecare director de aplicaţie. Iar potenţialele beneficii pe care le-ar aduce aplicaţiei X înlocuirea de către aplicaţia Y a unui dll comun cu o versiune mai nouă sunt mult mai mici decăt riscul ca noua versiune să fie incompatibilă cu aplicaţia X. Dacă aplicaţia X ar putea beneficia de o versiune mai nouă a dll-ului în cauză, ar fi de preferat ca aplicaţia X să fie recompilată, retestată şi re-deploy-ată cu noua versiune de dll. Pe calculatorul userului nu vor mai ajunge combinaţii aplicaţie/dll care nu au fost luate în calcul de către developer şi tester.
Altfel zis, nu mai încercăm să gestionăm dll hell-ul – ci pur şi simplu renunţăm la dll-urile comune. Microsoft-ul a tăiat nodul gordian.
Această nouă abordare pe care o aduce .Net-ul, ridică semne de întrebare asupra utilităţii Windows Installer-ului (msi-ului). La drept vorbind, Windows Installer-ul a fost creat pentru a gestiona dll hell-ul – şi tocmai am decis că nu mai vrem să gestionăm nimic – vom copia fiecare dll în directorul fiecărei aplicaţii. .Net-ul ar putea beneficia de pe urma unui sistem simplificat de deployment, care să pună în practică XCopy deployment şi să cuprindă suport pentru AutoUpdate.

Faceţi cunoştinţă cu ClickOnce.
ClickOnce permite copierea unei aplicaţii de pe un server de web spre exemplu în profilul userului care are nevoie de serviciile acelei aplicaţii. ClickOnce nu va face niciodata mai mult decat să aducă fisierele local şi să pună vreun icon pe desktop-ul acelui user. ClickOnce va rula întotdeauna sub credenţialele userului logat – sub nici o formă nu îşi propune să suporte scenarii gen Advertise, menite să dea posibilitatea unui user fără privilegii speciale să instaleze o aplicaţie care a fost aprobată în prealabil de un admin. Aprobarea admin-ului se va face pe viitor nu sub forma definirii unei liste de aplicaţii safe, pe care un user are dreptul să le instaleze (prin publish sau advertise) – ci mai curând prin specificarea unor politici de securitate (~CAS) pe care aplicaţiile instalabile trebuie să le respecte. Spre exemplu, s-ar putea defini ca userul să aibă dreptul să instaleze orice aplicaţie atâta timp căt acea aplicaţie nu necesită accesarea sistemului de fişiere sau nu cere să se conecteze la internet (no more spyware).
Frumoasă idee.
Dar ce lipseşte? Ei bine, m-aş simţi mai liniştit dacă aş avea siguranţa că soluţia e completă.
1. Pentru a motiva introducerea SmartClients-urilor, prezentările Microsoft aduc de fiecare dată în discuţie costurile de deployment pentru soluţiile clasice windows forms în Enterprise (se foloseşte SMS-ul pentru a speria puţin audienţa). Eu interpretez asta ca pe o promisiune din partea Microsoft de a susţine Smart Clients (= ClickOnce) ca alternativă la sistemele clasice de deployment. .Net 2,0 însă e doar jumătate din treabă.
E drept, fiecare user al meu ar avea posibilitatea să-şi instaleze aplicaţia pe profilul personal prin simpla accesare a unui URL. Întrebarea e : care url ? Există vreo modalitate la nivelul Enterprise-ului de a publica URL-urile aplicaţiilor către user ? Mă gândesc la un sistem care să înlocuiască Advertise-ul aplicaţiilor cu Active Directory GPOs, nu la mass mailing sau SharePoint. Userii din enterprise sunt obişnuiţi să-şi găsească pe desktop iconiţele de care au nevoie şi nu au obiceiul ca atunci când iconiţele le lipsesc să-şi verifice mailul în căutarea link-ului salvator. Aş vrea să aud şi vocea celor de la Active Directory cântând la unison cu cei de la .Net Framework.
2. .Net-ul aduce o revoluţie în problema dll hell-ului. Fiecare aplicaţie cu dll-urile sale. Gata cu incompatibilităţie. Însă în acelaşi timp, am renunţam şi la beneficiile dll-urilor partajate. Ei bine, uneori actualizarea unui dll comun putea fi benefic pentru toate aplicaţiile care îl foloseau. Acum însă, va trebui să actualizăm fiecare din aplicaţiile existente independent. Vă aduceţi aminte? Compilare – testare – deployment pentru fiecare aplicaţie, ori de câte ori un dll (fost) partajat a fost modificat. Doar aşa o aplicaţie poate beneficia de bug fix-urile din aceste dll-uri.
Costurile procesului de compilare – testare – deployment vor trebui însă să fie cât mai mici pentru a putea permite redeployment-ul fiecărei aplicaţii la fiecare fixare a unui bug într-o bibliotecă partajată. Pentru developer, asta se traduce în necesitatea unui sistem de automatizare a build-urilor care să cuprindă inclusiv faza de realizare a kitului aplicaţiilor. E absolut necesar ca generarea acelor manifeste (fişiere xml) care descriu kitul aplicaţiei să fie realizabilă nu doar dintr-un wizard (util pentru obţinerea primului kit) – ci şi în mod automatizat (linie de comandă) pentru a suporta build script-urile. Dacă Microsoft lasă in seama programatorului crearea uneltelor necesare regenerării acelor manifeste pe baza dependinţelor aplicaţiei se chemă că nu a făcut treaba decât pe jumătate.

Monday, May 02, 2005

Generarea codului cu CodeSmith

În episodul anteriror, am analizat avantajele folosirii propriilor clase în locul TypedDataSet-urilor şi am amintit de facilităţile la care suntem nevoiţi să renunţăm odată cu abandonarea TypedDataSet-urilor. E momentul să ne ocupăm de câteva din aceste probleme.
Probabil cea mai mare piedică în adoptarea propriilor clase în locul TypedDataSet-urilor este cantitatea de cod care ar trebui scrisă – capabilă să înspăimânte şi să omoare prin plictiseală chiar şi pe cel mai meticulos programator. Pentru a trece mai uşor peste acest impediment, putem folosi generatoare de cod care, pornind de la un tabel din baza de date, să ne ofere un punct de plecare pentru obiectele noastre.
O căutare cu google după “code generator" o să vă dezvăluie cât efort a fost depus de-a lungul timpului în direcţia generatoarelor de cod. Există incredibil de multe soluţii – multe din ele specializate pe generarea de cod pentru DataLayer, altele care promit generarea unor întregi site-uri plecând de la schema unei baze de date. Am ales CodeSmith pentru că este o unealtă nespecializată, care nu ne impune un anumit model de programare sau o anumită arhitectură şi, în plus, are o sintaxă foarte apropiată de Asp.Net – deci familiară multora dintre noi.
Trebuie însă să menţionez că e vorba de un generator de cod gratuit însă nu open-source. Doar binarele pot fi descărcate de la adresa http://www.ericjsmith.net/codesmith/ . Produsul e suficient de matur pentru a putea fi folosit în medii profesionale iar site-ul autorului oferă şi câteva resurse de asistenţă (câteva tutorialuri şi un forum de suport). Pe lângă generatorul propriu zis (care e gratuit) există şi un editor comercial de templateuri – CodeStudio - care oferă facilităţi suplimentare precum sintax highlight-ing.

Template-urile CodeSmith îndeplinesc câteva funcţii diferite :
· declară setul de parametrii de intrare
· definesc un şablon al codului care va fi generat
· suportă adăugarea de cod C#/VB.Net/JScript care să manipuleze parametrii şi sablonul

Pentru început, să încercăm să facem un template de generare a colecţiilor tipizate (typed collections). Rezultatul ar trebui să arate cam aşa pentru o colecţie de obiecte Salariat :

public class SalariatCollection : System.Collections.CollectionBase
{
public Salariat this[int index]
{
get{ return (Salariat) this.List[index];}
set{ this.List[index] = value;}
}

public int Add(Salariat valoareNoua)
{
return this.List.Add(valoareNoua);
}

public void Remove(Salariat valoareDeSters)
{
this.List.Remove(valoareDeSters);
}
}

iar pentru o colecţie de Funcţii, rezultatul trebuie să fie destul de simetric :

public class FunctieCollection : System.Collections.CollectionBase
{
public Functie this[int index]
{
get{ return (Functie) this.List[index];}
set{ this.List[index] = value;}
}

public int Add(Functie valoareNoua)
{
return this.List.Add(valoareNoua);
}

public void Remove(Functie valoareDeSters)
{
this.List.Remove(valoareDeSters);
}
}

Comparând cele două exemple, punctele comune şi diferenţele ies imediat în evidenţă:

public class xxxxCollection : System.Collections.CollectionBase
{
public xxxx this[int index]
{
get{ return (xxxx) this.List[index];}
set{ this.List[index] = value;}
}

public int Add(xxxx valoareNoua)
{
return this.List.Add(valoareNoua);
}

public void Remove(xxxx valoareDeSters)
{
this.List.Remove(valoareDeSters);
}
}

Prin urmare, template-ul nostru va trebui să aibă un parametru prin care să putem furniza tipul-ţintă pentru care se generează colecţia.
Pentru a scrie template-ul, deschideţi CodeSmith Explorer (găsiţi optiunea în menu-ul Tools din Visual Studio); selectaţi ca editor de template-uri Notepad-ul din directorul Windows


apoi creaţi un template nou în care scrieţi codul următor :

<%@ CodeTemplate Language="C#" TargetLanguage="C#" %>
<%@ Property Name="TipDeBaza" Type="System.String" %>
<%@ Property Name="NumeNamespace" Type="System.String" %>

namespace <%=NumeNamespace%>
{
public class <%=TipDeBaza%>Collection : System.Collections.CollectionBase
{
public <%=TipDeBaza%> this[int index]
{
get{ return (<%=TipDeBaza%>) this.List[index];}
set{ this.List[index] = value;}
}

public int Add(<%=TipDeBaza%> valoareNoua)
{
return this.List.Add(valoareNoua);
}

public void Remove(<%=TipDeBaza%> valoareDeSters)
{
this.List.Remove(valoareDeSters);
}
}
}
Salvaţi şi executaţi template-ul din CodeSmith Explorer, şi veţi obţine un property grid în care vi se cere să furnizaţi valori pentru cei doi parametrii declaraţi mai sus:




Un click pe butonul de Generate şi veţi obţine codul aşteptat. Schimbând valoarea parametrului TipDeBază veţi putea obţine codul de TypedCollection pentru orice tip de date de care aţi avea vreodată nevoie. Atenţie însă – codul rezultat este un cod demonstrativ, simplificat în mod special pentru acest articol (nu conţine o implementare completă a unei colecţii typed); între exemplele cu care se distribuie CodeSmith sau pe forumul de asistenţă veţi găsi câteva template-uri de colecţii tipizate mult mai elaborate.
Cu toate că sunt foarte utile astăzi, colecţiile tipizate generate cu CodeSmith îşi vor pierde probabil fanii odată cu introducerea Generics-urilor din C# 2.0 . CodeSmith nu se limitează însă la generarea colecţiilor tipizate ...
Folosirea property grid-ului pentru introducerea valorilor parametrilor este probabil una dintre cele mai inspirate decizii ale autorului, întrucât property grid-ul oferă un suport teribil pentru extensibilitate. Aşa se face că în template-urile CodeSmith putem declara şi edita parametrii de orice tip - fie el tip de bază, tip definit de utilizator sau colecţie. Practic, se utilizează aceeaşi infrastructură care permite invocarea în Visual Studio a unor editoare de proprietăţi precum cele de alegere a culorilor, de Dock şi Anchor sau de editare a colecţiilor. E suficientă decorarea unui tip de date cu atributul Editor pentru a specifica ecranul ce va fi deschis pentru alegerea valorilor proprietăţilor de acel tip.
Acest mecanism a fost folosit de autorul CodeSmith pentru a oferi suport pentru generarea codului pornind de la elemente din baze de date. În câteva assembly-uri auxialiare (SchemaExplorer.dll, SchemaExplorer.ADOXSchemaProvider.dll şi SchemaExplorer.SqlSchemaProvider.dll) sunt declarate clase care expun caracteristicile bazelor de date, ale tabelelelor, view-urilor sau procedurilor stocate. Toate aceste tipuri au asociate editoare care permit o selecţie foarte facilă a sursei lor. Spre exemplu, un template cu o proprietate de tipul SchemaExplorer.TableSchema va determina afişarea în property grid a unui butonaş cu trei puncte,


care permite deschiderea unui ecran de selecţie a bazei de date şi a tabelului sursă :




Aceste facilităţi ne vor fi foarte utile pentru problema noastră: va trebui să punem pe picioare un template care să ceară drept parametru o tabelă din baza de date şi să genereze câte un property get şi un property set pentru fiecare coloană a tabelei sursă. Ceva de genul

public class Salariat
{
private string _nume;
public string Nume
{
get{return _nume;}
set{_nume = value;}
}
private string _prenume;
public string Prenume
{
get{return _prenume;}
set{_prenume = value;}
}
private DateTime _dataNasterii;
public DateTime DataNasterii
{
get{return _dataNasterii;}
set{_dataNasterii = value;}
}
……….
}

Cel mai sensibil aspect e declararea parametrului de tip TableSchema; întrucât tipul SchemaExplorer.TableSchema este definit într-un assembly auxiliar (nu în core-ul generatorului de cod) va trebui să folosim directiva <%@Assembly %> pentru a face o referinţă la acest assemby :

<%@ CodeTemplate Language="C#" TargetLanguage="C#" %>

<%@ Assembly Name="SchemaExplorer" %>

<%@ Property Name="TabelaSursa" Type="SchemaExplorer.TableSchema" %>
<%@ Property Name="NumeNamespace" Type="System.String" %>

namespace <%=NumeNamespace%>
{
public class <%=TabelaSursa.Name%>
{
}
}

După cum se observă, am folosit expresia TabelaSursa.Name pentru a obţine numele tabelei ce a fost selectată ca parametru. Tipul proprietăţii TabelaDeBază - SchemaExplorer.TableSchema - oferă probabil prin proprietăţile sale orice informaţie de care aţi avea vreodată nevoie de la o tabelă, incluzând informaţii complete despre coloanele tabelei, despre cheia primară, despre indecşi, despre ralaţiile cu alte tabele sau despre proprietăţile ei extinse.
Putem folosi deci proprietatea TabelaSursa.Columns, de tipul ColumnSchemaCollection, pentru a enumera coloanele tabelei. Pentru fiecare coloană, proprietatea Type ne va oferi tipul de date nativ bazei de date (spre exemplu nvarchar) în timp ce proprietatea SystemType ne va oferi tipul echivalent in C# (string).

<%@ CodeTemplate Language="C#" TargetLanguage="C#" %>

<%@ Assembly Name="SchemaExplorer" %>

<%@ Property Name="TabelaSursa" Type="SchemaExplorer.TableSchema"%>
<%@ Property Name="NumeNamespace" Type="System.String" %>

namespace <%=NumeNamespace%>
{
public class <%=TabelaSursa.Name%>
{
<%
foreach (SchemaExplorer.ColumnSchema coloana in TabelaSursa.Columns)
{
%>

private <%=coloana.SystemType%> _<%=coloana.Name%>;
public <%=coloana.SystemType%> <%=coloana.Name%>
{
get{ return _<%=coloana.Name%>; }
set{ _<%=coloana.Name%> = value;}
}
<%
}
%>
} //de la numele clasei
} // de la namespace

In template-ul de mai sus, trebuie remarcat codul scris italic bold. Codul scris între tag-urile <% si %> nu va apare în cadrul rezultatului generării, ci va fi rulat la execuţia template-ului – în cazul de faţă determină repetarea secvenţei imbricate pentru fiecare coloană a tabelei.
Template-ul nostru este funcţional – şi generează deja câte o proprietate get/set pentru fiecare coloană din tabela furnizată ca parametru. E un punct de plecare care are încă nevoie de suficiente îmbunătăţiri; spre exemplu, am putea suprima generarea property set-ului pentru coloanele Identity, încadrând codul property set-ului într-un if:
<%
if ( (bool)coloana.ExtendedProperties["CS_IsIdentity"].Value != true)
{
%>
set{ _<%=coloana.Name%> = value;}
<%
}
%>

La fel de uşor, am putea să adăugăm la acest template şi scriptul de generare a colecţiilor tipizate de la începutul acestui articol. Pentru a păstra modularitatea, cel puţin în cazul template-urilor mai complexe, ar fi indicat să nu concatenăm cele două fişiere, ci să apelăm TypedCollectionsTemplate ca pe un subTemplate. În acest scop, vom scrie următorul cod:
<%
Response.WriteLine("// si acum, colectia typed");
CodeTemplateCompiler compiler = new
CodeTemplateCompiler(this.CodeTemplateInfo.DirectoryName +
"TypedCollectionTemplate.cst");
compiler.Compile();
CodeTemplate subtemplate = compiler.CreateInstance();
subtemplate.SetProperty("TipDeBaza", TabelaSursa.Name);
subtemplate.SetProperty("NumeNamespace",NumeNamespace);
subtemplate.Render(Response);
%>
Vom continua în episoadele urmâtoare să completăm acest template, în măsura în care dezvoltarea obiectelor noastre ne va cere noi facilităţi (spre exemplu, vom discuta despre suportul coloanelor ce permit null sau suportul pentru limitarea lungimii maxime a string-urilor în funcţie de dimensiunea specificată în baza de date).
Cu toate că posibilităţile de generare a codului cu CodeSmith sunt impresionante (nu am văzut deocamdată decât vârful iceberg-ului) e important să ţinem cont că nu ne-am propus să generăm integral clasele noastre de business cu CodeSmith. În primul rând, aceste clase nu sunt întotdeauna în relaţie 1-la-1 cu tabelele din baza de date; pe de altă parte, aceste clase vor trebui să implementeze regulile de validare sau de calcul specifice fiecărei entităţi, care nu pot fi deduse din schema bazei de date şi nici nu ar putea fi exprimate sub forma parametrilor. Rolul CodeSmith în generarea obiectelor de business se reduce la generarea unui cod funcţional, care să poată fi compilat şi testat imediat, dar care este doar un punct de plecare, un schelet pe care să-l putem modifica şi peste care să putem adauga manual codul propriu ori de câte ori regulile de business o cer.
Datorită flexibilităţii deosebite, CodeSmith poate fi o unealtă extrem de puternică, cu aplicaţii din cele mai diverse. Putem genera la fel de uşor orice fel de cod, fie el C#, VB.Net, JavaScript sau TSQL. (Spre exemplu, am putea face un template care să primească ca parametru o bază de date şi să genereze un script conţinând toate procedurile stocate de insert/update/delete pentru fiecare tabelă, sau triggere pentru un log al modificărilor asupra datelor din tabele.) Template-urile CodeSmith pot fi invocate şi cu un utilitar command line, din fisiere .bat, sau din IDE cu un CustomTool asemănător celui care generează TypedDataSet-uri din fişiere XSD. Vom reveni asupra integrării în IDE prin CustomTool în unul din episoadele următoare, în care vom genera un pseudo-DataLayer pentru a asigura funcţiile de persistenţă ale obiectelor noastre.

Saturday, April 30, 2005

Episodul pilot

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.

Saturday, March 26, 2005

Ziua unui programator - de la entuziasm la deprimare

10.03
Gmail, hotmail, outlook.
Am terminat toate mailurile de citit.
Si cafeaua de baut.
Nici macar pe cele doua-trei forumuri pe care le monitorizez nu se mai intampla nimic.
Asa ca pana la urma, tot va trebui sa incep sa lucrez.
Si totusi ... daca intre timp o mai fi venit ceva pe Gmail ?

10.27
Tocmai m-a palit IDEEA.
Acum mi-e clar ce am de facut pentru noul feature cerut de client.
Ma apuc furibund de batut in tastatura ca la fasole. Ideeile vin ceva mai repede decat reusesc sa debitez garla asta de property get si set-uri...
E clar, imi trebuie inca o clasa. Deschid un fisier nou si incep sa schitez noua clasa.

12.41
Degetele au inceput sa-mi ezite.
Probabil aici o sa-mi trebuiasca ceva configurari .. Notez in todo list .. Iar bucata asta de cod va trebui revizuita. Nu mi-e prea clar daca o sa ruleze din prima ..
O sa trebuiasca sa-mi aduc aminte sa mai revin peste asta o data... Poate ar trebui sa imi pun un #warning.

12.55
In sfarsit.
Asta a fost.
CTRL+SHIFT+B.
Building...
Una, doua, cinci .. vreo sase erori.
Litere mancate, linii ramase neterminate. O paranteza lipsa. Nimic grav.

12.59
F5 - Run.
Aplicatia porneste.
Si ce daca ?
Am uitat sa adaug noul form in menu.

13.01
F5 din nou.
Deschid nerabdator formul proaspat (inca arata cam "deranjat").
Introduc cateva date la intamplare in textbox-urile required.
Apas OK(Save) pentru a salva in baza de date ..
Error.
Mda.
Stop debugging. O mica corectie.

F5 din nou.
Deschid formul.
Introduc datele la intamplare (oare or fi la fel ca mai devreme?).
Ok(Save)....
Error.
Fir-ar.. Ah .. uitasem. Aici ar mai fi trebuit pus un if pentru cazul asta special...

F5 din nou.
Deschid formul. Introduc datele la intamplare (pe cine pacalesc ? nu mai am atata imaginatie .. e clar ca sunt tot datele de mai devreme).
Ok(save)...
Error.
Ah,da. Cred ca aici de fapt ar fi trebuit i+1.

F5 din nou.
Deschid formul.
Introduc datele mecanic.
OK (save) ..
Error.
Lumea sa prabuseste in jurul meu.
Ce o mai fi si de data asta ?
Mi-e somn.
Si greata.
Simt ca ma paste o depresie.
Urasc tasta F5. Si croncanitul hardului in timp ce face building. Si mesajul ala urta de eroare.

Oare o mai fi venit ceva pe Gmail ?


...
Asa arăta o zi de lucru pentru mine - de la entuziasm la depresie.Am avut intotdeauna dificultati in a pastra un ritm sustinut intr-o activitate (si am toata admiratia pentru cei care reusesc sa faca asta fara un efort deosebit).Pentru mine insa lucrurile nu's chiar roze - ma plictisesc repede, si imi pierd energia. Pentru mine un ritm sustinut de lucru cere eforturi supraomenesti. Si o multime de dulciuri sau alte chestii rontzaibile (chestie foarte usor de constatat dealtfel).
Un ritm constant necesita muzica potrivita.
Si cat mai putine lucruri descurajante.
Sau deprimante.
Precum sedintele lungi de debugging.

Ahh ...sedintele astea lungi de debuging .
Chestiile astea ma ingroapa.
Daca toti programatorii ar iubi la fel de tare ca mine sesinile de debugging probabil ca bransa asta ar fi pe cale de disparitie - am avea toti ori tendinte suicidale, ori de ucigas in serie.
Secventele astea de Run - introducere date - Bang (error) sunt inumane. Si le gasesc cu atat mai enervante cu cat vin din VB6 unde obisnuiam sa modific codul, sa dau "instruction pointerul" cu doua linii mai sus si sa probez imediat modificarea.
Dar nu, in Visual Studio 2003 nu se mai accepta astfel de gesturi de hobbist. VS-ul e "Proffesional" - si profesionistii dat cod impecabil din prima.
Sau trebuie sa opresca sesiunea de debugging , sa modifice codul, sa dea iar F5, sa priveasca iarasi rabdatori mesajele cu building .... sa caute iarasi formul in cauza prin menu-ul aplicatiei, sa introduca iarasi datele de test ...totul in speranta ca "de data asta o fi bine".
In timp insa, am uitat de cat de placut era sa faci Edit and Continue (stiu, o sa-mi aduc aminte iar in VS2005).
Trecerea de la "Edit and Coninue" la "Restart and Pray" era pe cale sa ma determine sa ma apuc de fumat.
Insa am scapat la limita.
Ce ma enerva atat de tare? Pierdeam mult prea mult timp incercand sa reproduc contextul in care aparea eroarea. Imi lua prea mult sa aleg optiuni de menu-uri, sa completez zece textbox-uri pe ecran inainte sa pot da iarasi "salveaza in baza de date" ca sa-mi ajunga iarasi executia la breakpoint-ul care ma interesa. In plus, toata rutina asta cu introducerea acelorasi date in textbox-uri e teribil de monotona - iar eu urasc lucrurile monotone si repetitive.
Chinul de a introduce mereu aceleasi valori in textbox-uri mi-a adus aminte de anii de bajbaiala de prin liceu, cand ma luptam cu cine stie ce algoritm de drum optim ... Lucram in C. Si aveam aceeasi problema enervanta - trebuia sa introduc iar si iar aceeasi parametrii de intrare si sa examinez de fiecare data rezultatele (asta cand nu reuseam sa resetez calculatorul din cauza vreunui pointer neinitializat). Pentru programasele mele scrise in liceu in C (aplicatii de DOS) gasisem insa o metoda foarte practica de a scapa de calvarul datelor de intrare: scriam intr-un fisier text toate valorile de intrare, despartite prin cate un Enter. Apoi redirectam console input-ul spre fisierul asta. Pentru redirectare era suficienta o singura linie de cod - toate scanf-urile alea incepeau sa citeasca din fisier in loc sa astepte input de la tastatura - iar eu scapam de toata corvoada introducerii repetate a datelor de intrare. Stiu, nu-i nimic special in treaba asta - probabil si voi ati folosit sistemul redirectarea consolei.
In VS2003 , cand mi-am dat seama ca trebuie sa restartez aplicatia de prea multe ori si sa introduc de fiecare data nishte valori in textbox-uri... a inceput sa-mi fie dor de redirectarea consolei din aplicatiile de DOS.
Asa ca am pus un buton pe form. Am scris pe el "Date de test" , iar la buton_click am completat din cod toate textbox-urile cu valorile alea plictisitoare. La fiecare pornire a programului - trebuia sa caut formul in cauza prin menu-ul aplicatiei, sa-l deschid si sa apas pe butonul de test si toate campurile se comletau cuminti cu nishte valori. Evident, dupa ce rezolvam bug-ul, stergeam butonul care nu trebuia sa ajunga la client :-).
Mi s-a intamplat de cateva ori insa sa revin dupa o zi-doua la acelasi form - cu un alt bug (evident, mai modificasem cate ceva si pocnise iarasi...).Si uite asa am inceput sa regret repede butoanele sterse .. . In scurt timp am ajuns sa nu mai sterg butoanele cu pricina si codul atasat lor - ci doar sa le pun proprietatea visible pe false. Cand aveam nevoie de ele - era suficient sa le pun visible inapoi pe true.
Rezolvam bug-ul - inapoi cu visible false.
M-am gandit sa folosesc in locul unui buton obisnuit un control derivat din button dar care sa-si seteze singur proprietatea visible in functie de modul in care se compila aplicatia (in Debug sau in Release) sau in functie de vreo variabila statica de la nivelul aplicatiei.Pentru unele formuri mai pretentioase, m-am gandit ca mi-ar putea prinde bine chiar 2-3 butoane pentru a acoperi doua-trei seturi de valori diferite. (Sau poate ca ar fi trebuit sa folosesc in loc de buton un combobox din care sa pot alege setul de date ?).
Dar intre timp - pentru ca nu mai pierdeam timp cu introducerea datelor in textbox-uri - a inceput sa ma sacaie succesiunea de click-uri prin menu-ul aplicatiei si pe butoane de test pe care trebuia sa le dau pentru a deschide formul pentru care faceam debugging.
Si asa mi-am dat seama ca imi lipseste Access-ul - in care poti da click dreapta -> Form view pe orice form aflat in design. Imediat formul trece in Run mode, si-l poti proba si apasa pe toate butonasele - se comporta exact cum se va comporta in fata utilizatorului.
Imi trebuia o modalitate de a porni direct un form, evitand menu-ul aplicatiei si alte formuri intermediare.
Solutia - mai intai am incercat sa schimb form-ul de start-up al aplicatiei. Trebuia insa de fiecare data sa repun lucrurile la loc dupa ce rezolvam bug-ul.Pana la urma mi-a fost mai simplu ca in solution-ul curent sa adaug un alt proiect, cu o referinta la proiectul principal. Puteam modifica de oricate ori vroiam functia main a acestei aplicatii auxiliare (aplicatie de test) - pentru ca asta oricum nu ajungea la client si nu prea conta in ce stadiu ramanea functia ei main.De cate ori era nevoie sa mai rezolv un form marcam proiectul de test ca Start-up project si in functia lui main, apelam formul care facea probleme. Adio splash screen-uri pe care trebuia sa le vad de sute de ori, adio menu-uri. Formul vinovat pornea direct la F5. Un singur lucru mai ramanea de facut - sa apas butonul de test de pe el pentru a completa automat textbox-urile cu valorile predefinite.Si sa dau OK(save) sa vad daca mai obtin vreo eroare.
De click-urile astea am scapat usor - pentru ca din functia main a aplicatiei puteam instantia formul si apela direct buttonTest_click , apoi ButtonSave_Click. Cu conditia ca aceste functii sa fie publice.
Sau sa folosesc reflection (pentru ca poti apela functii private folosind reflection).
Reflection-ul insa mi-a dat o alta perspectiva asupra lucrurilor. Cu Reflection poti cauta toate functiile asociate butoanelor de test dintr-un form (sau dintr-un intreg assembly). Si sa afisezi toate aceste nume de functii intr-o lista (poate chiar un tree). La pornirea aplicatiei auxiliare poti selecta oricare din functiile afisate si sa o invoci cu dublu-click.La drept vorbind nici nu mai aveam nevoie de butoanele de test de pe formuri - ci doar de codul lor din button_click, codul prin care se completau textobox-urile.
Pentru a putea filtra insa functiile de test de celelalte functii ale aplicatiei avem nevoie de o conventie de denumire. (Spre exemplu, toate aceste functii sa se termine in Test). Sau sa fie marcate printr-un atribut folosit ca un marker. Explorer de functii de test cauta si afiseaza toate metodele care se termina in Test sau sunt marcate cu un anumit atribut.
Asadar, la fiecare pornire a aplicatiei de test am afisat lista functiilor de test din aplicatia principala si cu un dublu click am pornit formul si am completat textbox-urile lui cu valorile de test.
In felul asta am scapat de nevoia de a modifica mereu Main-ul aplicatiei auxiliare pentru a porni unul sau altul din formuri. Mai mult - aplicatia auxiliara a devenit suficient de generica pentru a putea fi refolosita cu oricare din proiectele la care lucram.
Ce am obtinut in felul asta?O modalitate foarte complicata de a completa nishte default-uri prin form-uri :-).
Big deal.
Intr-o zi insa listul de selectie a functiei de test s-a transformat intr-un multiselect.
Chestie foarte utila pentru cazul in care faceam modificari in nucleul aplicatiei, sau intr-o functie apelata din mai multe formuri. Puteam modifica linistit codul si apoi sa rulez toate functiile de test din toate formurile pentru a verifica daca aplicatia nu cumva crapa in urma noii modificari. Formurile se deschideau unul cate unul, functiile de test completau datele predefinite, butoanele de save se apelau automat.
Grotesc, nu ?
Daca totul decurgea normal cele 50 de functii se executau una dupa alta iar eu puteam zambi fericit - modificarea din core-ul aplicatiei nu a aruncat in aer nimic de data asta.
Chestie care incurajeaza lucrul la functiile comune (din "core"). Inainte aveam retineri daca era nevoie sa ma ating de o functie din nucleul aplicatiei, care ar putea avea impact in prea multe locuri.

Ati rezistat pana aici ?
Un singur lucru mai lipseste noului noustru mod de lucru.
Un nume.
Si in cautarea unui nume vom afla ca nimic nu e nou sub soare si n-am facut decat sa reinventam roata.
Numele este deja ocupat - povestea mea seamana oarecum a Test Driven Developement.
Cred ca Test Driven Developement nu trebuie impus ca o metodologie de lucru, ca un nou stil in care sa se lucreze. Am ajuns la TDD analizand activitatea zilnica si incercand sa scap de chestiunile chinuitoare. La drept vorbind cu totii facem din prima zi Test Driven Developement - doar dupa fiecare modificare de cod oricum dam un run pentru a proba daca programul merge asa cum trebuie.
Run - Input - Action - Check Results.
Asta e procesul.
Odata automatizata activitatea de "completare de valori de intrare" am tendinta de a rula din ce in ce mai des testul. Dupa orice modificare minora - run test. Treaba asta cu run test imi da un sentiment de siguranta. Chiar daca testul in cauza nu acopera decat o mica parte din scenariile posibile - faptul ca vad inca o data bucatica mea de cod ruland asa cum trebuie imi da incredere si imi insufla un dram de optimism. Ma ajuta sa pastrez un ritm constant. Si tzine loc de dulciuri.
Contrar aparentelor, rularea repetata (obsesiva chiar) a testelor nu diminueaza productivitatea. E chiar invers. Teste mai dese inseamna bucati mici de cod intre ele. Daca testul era ok acum 3 minute si intre timp n-am scris decat 10 linii si acum testul crapa - e clar , bug-ul a fost introdus undeva in cele 10 linii de cod. Mi-e mult mai simplu decat daca au trecut 2 ore si 10 clase de la ultima pornire a aplicatiei.
Se pare ca timpul necesar debugg-ing-ului creste exponential cu cantitatea de cod scrisa de la ultima sesiune de debugging.
Atunci de ce nu dam Run mai des? Pentru ca avem datele alea de introdus mereu. Trebuie sa refacem contextul testului de fiecare data. Si cum asta e o activitate plictisitoare, de durata si care tinde sa rupa ritmul programarii, nu prea o facem des.
Imediat insa ce automatizam procesul de refacere al contextului , rularea unui test dureaza doar cateva secunde. Si in loc sa intrerupa ritmul lucrului, ii formeaza o cadenta.
In scurt timp nici nu mai e nevoie de debugg-ing - adica de breakpoint-uri si inspectarea variabilelor. Pentru ca mereu bug-ul e undeva in ultimele 10 randuri de cod scrise.
Morala povestii ?
Test driven developement nu iti impune cu forta o arhitectura n-tier. Test Driven Developement nu inseamna musai NUnit.
Test Driven Developement inseamna mai mult un mod mai interesant de a aborda problemele.
Nu va ganditi la Test Driven Developement ca la o masura de asigurare a calitatii. (Sa fim seriosi, in clipa in care spui asigurarea calitatii, jumatate de sala se goleste)
Ganditi-va la TDD ca la o metoda de reducere a timpilor de debugging si crestere a eficientei.
Sau ca la o metoda de a mai condimenta putin viata asta plictisitoare de programator :-p .