De kwaliteit van variabelen in je broncode
Dit nummer van het SDGN magazine is gewijd aan “Quality Assurance”. Voor ons als code kloppers is dat misschien een wat vaag begrip, maar ik wil in dit verhaal duidelijk maken dat ook wij een hele grote bijdrage kunnen leveren aan de kwaliteit van een applicatie. Een aanzienlijk gedeelte van de slechte kwaliteit daarvan is te wijten aan programma-fouten. In dit verhaal wil ik laten zien hoe je je ontwikkelgereedschap beter kunt gebruiken om een aantal van deze fouten zelf te vinden in plaats van dat de gebruiker er op een (niet zo) goed moment tegenaan loopt. Ongetwijfeld trap ik een hoop open deuren in, maar je zult de voorbeeldcode de kost moeten geven waar deze deuren nog potdicht zitten.
Het grote verschil tussen een script en een gecompileerde source is dat een te compileren source bij de ontwikkelaar door de compiler gaat en dat een script pas wordt verwerkt op het moment van uitvoeren.
In dit verhaal ga ik kijken naar het gebruik van variabelen in een aantal talen, zowel scripttalen als gecompileerde talen. Script is de enige taal die een webbrowser begrijpt, je kunt daar dus niet omheen. Naast VBscript komen ook “echte” talen zoals Delphi en C# aan bod. Ik zal laten zien dat alleen het schrijven in een “echte” taal nog niet voldoende is om de werkelijke voordelen van die taal ook daadwerkelijk te benutten.
Scripts
Het grote verschil tussen een script en een gecompileerde source is, dat een te compileren source bij de ontwikkelaar door de compiler gaat en dat een script pas wordt verwerkt op het moment van uitvoeren. Compileren heeft vele voordelen. Behalve dat het snellere code oplevert, controleert de compiler de source ook op (onder andere) syntaxfouten. Of het script klopt, merk je soms pas op een heel laat moment.
Laten we eens naar een praktijk voorbeeld kijken.

Iedere keer dat het document wordt geopend wil dit script laten zien hoeveel dagen het nog duurt voor de maandcijfers er zijn. Helaas klopt de syntax niet, je browser zal gaan klagen als je de pagina opent :

Bij het opbouwen van de pagina controleert de browser het script op syntax. De ontwikkelomgeving (MS script editor) vond het allemaal prachtig en liet met highlights zien waar de if’s en de else’s stonden. Maar dat de laatste end if ontbrak zag die niet. De browser wel. Het euvel is snel gekorrigeerd:

Nu loopt de pagina als een trein. Maar als de grote dag eindelijk aanbreekt dan is het feest toch weer afgelopen:

Deze foutmelding is niet eens zo duidelijk; wat die precies betekent, daar kom ik straks op terug. De oorzaak van het probleem is dat allert in het ene geval met dubbel l was geschreven en in het andere met maar één l. De laatste spelling was de juiste, alleen die staat voor een bestaande methode.
De moraal van dit verhaal is dat een script, welliswaar pas bij het laden, wordt gecontroleerd op syntax fouten maar dat bij het daadwerkelijk uitvoeren van de regel code pas blijkt of er gebruik wordt gemaakt van bestaande variabelen, properties of methodes.
Late en early binding
Het probleem in het script zat hem erin dat de script interpreter de allert methode van het Window object niet kon vinden. Scripts maken volop gebruik van COM (OLE) objecten. De explorer omgeving in de browser kent het Window COM object. Dit object heeft een methode Alert die een melding aan de gebruiker toont. De methodes van een COM object kunnen op verschillende manieren worden aangeroepen (zie ook : “Anatomie van een automation object” in SDGN magazine # 69), de meest flexibele manier is late binding en loopt via de GetIDsOfNames methode. Elk COM object kent deze, hij krijgt de naam van gewenste method of property als string mee en levert het een en ander aan info op. De Invoke methode van het COM object gebruikt deze info om de methode daadwerkelijk uit te voeren. Dit is precies de werkwijze die de script runtime volgt om de code uitgevoerd te krijgen. Als de call naar GetIDsOfNames niets oplevert, zoals in het Allert geval, dan krijg je de foutmelding.
Dit late binding scenario kom je niet alleen in scripts tegen. Laten we eens met een echte gecompileerde taal, Delphi bijvoorbeeld, met COM objecten gaan werken.

De code maakt een COM (OLE) object aan en leest een property. Aan de blauwe puntjes kun je zien dat de compiler het prima vindt, maar bij het uitvoeren van het programma gaat het toch mis.

Je krijgt een error. De Delphi compiler heeft achter de schermen, net als de script runtime, de call naar het COM object vertaald naar GetIDsOfNames en Invoke. De naam van de property is intern gedegradeerd tot een string. En ook hier gaat het bij het draaien van het programma pas mis.
Gelukkig kan Delphi, in tegenstelling tot de script omgeving, heel goed overweg met de type informatie (TypeInfo) die een COM object te bieden heeft. Door de typelibrary van de COM server in je projekt te importeren krijg je de beschikking over heel wat strakkere types dan een OleVariant. Zo is er de IfileZapper interface en de CoFilezapper class. En zo kan de compiler al zien waar de fout zit.

De compiler verzorgt nu de binding van de property namen aan de members van het COM object. Dit heet officieel vtable binding, maar wordt meestal early binding genoemd. (Voor het verschil tussen vtable binding en early binding verwijs ik weer naar eerdergenoemd verhaal in SDGN Magazine #69). Het moge duidelijk zijn dat je gebruik moet maken van early binding waar je maar kan. Alleen al omdat de compiler fouten eruit haalt die anders maanden later, op een vreselijk ongelukkig tijdstip, op zouden duiken.
Code completion
De getoonde fouten zijn een gevolg van spelfouten. In plaats van zelf de namen van de methodes in te kloppen kennen de meeste ontwikkeltools code-completion, waarbij je de naam van de methode of property kunt kiezen uit een lijstje. Deze tool vind je ook in ontwikkeltools voor scripts, MSE had je zo kunnen vertellen dat alert de juiste methodnaam was in de window class:

Helaas doet MSE verder weinig met deze info. Als je zelf toch wat anders inklopt, dan vindt MSE dat ook wel goed. Code completion in Delphi maakt gebruik van de informatie die de compiler levert. Het spreekt dan ook boekdelen dat het code completion lijstje van een OleVariant helemaal leeg is.
De problemen die opdoken bij late binding komen eigenlijk allemaal voort uit iets dat ik verkapte strings zou willen noemen.
SQL strings
De problemen die opdoken bij late binding komen eigenlijk allemaal voort uit iets dat ik verkapte strings zou willen noemen. In je code ziet het er uit als een identifier, maar in de praktijk is het niet meer dan een string variabele die door wordt gegeven aan de echte code. De meeste database-code staat vaak ook helemaal vol met strings en deze code loopt dus hetzelfde risico. Laten we hier eens naar gaan kijken.
Menig SQL statement kom je in je code tegen als een hard gecodeerde string. Deze string is dan property of parameter voor de een of andere SQL component. In de eerste plaats moet je je natuurlijk afvragen of je source überhaupt wel de juiste plaats voor SQL statements is. Eigenlijk horen die in de database. Daarin maak je views en stored procedures aan die de SQL uitvoeren en (eventueel) het resultaat teruggeven. De tTable componenten in Delphi werken net zo makkelijk met een view als met een echte tabel uit je database. In de tStoredProc componenten is ook nergens plaats voor SQL. Resteren nog de tQuery componenten. Deze kunnen ontzettend nuttig zijn als je programma zelf een selectie in elkaar zet. Je laat je programma in een string de WHERE clause in elkaar zetten en het resultaat laat je los op de database. Maar als je programma altijd dezelfde query uitvoert dan ben je een stuk beter af met een tTable of een tStoredProc die gebruikt maakt van een query in je database.
VS.NET benadert een database altijd via een data-adapter component. Hierin heb je altijd een stukje SQL nodig om de data te pakken te krijgen. De vraag is nu hoe je design time kunt controleren of die SQL klopt. Een gedeeltelijke oplossing biedt hier de SQL query builder van VS.NET; deze helpt je je SQL statement op te bouwen.

Deze wizard haalt een syntax fout uit je query, maar hij controleert niet op kolom- en tabel-namen.
Een iets andere manier om te controleren of je SQL in orde is is om de data design time daadwerkelijk te openen. Delphi opent in de IDE sowieso alle datasets waarvan de Active property op true staat. Je krijgt de gegevens dan meteen te zien in alle data-aware componenten waar ze gebruikt worden. Dat ziet er best leuk uit, maar als je dat onbeperkt gebruikt, betekent dat in de praktijk dat je applicatie bij het opstarten alle data meteen gaat openen, aangezien de datasets op Active staan. VS.NET doet het iets subtieler. Bij een dataAdapter kun je op Preview Data klikken, waarna, na het opgeven van eventuele parameterwaardes, je de data kunt bekijken en controleren.
De moraal van dit verhaal is mijns inziens dat je je SQL statements zover mogelijk moet verstoppen. Het liefst in de database zelf en als dat niet kan in de properties van een component waar de property editor er nog enige controle over heeft.
Veldnamen in Delphi
Als het eenmaal gelukt is je data te pakken te krijgen wil je er ook wat mee doen. Om de individuele velden van een dataset in Delphi te pakken te krijgen heeft tDataSet een handige methode, FieldByName, deze levert een tField object op. Dit object heeft weer handige methodes om de inhoud van het veld als string of als ander type te benaderen. Delphi is zeer te spreken over de volgende statements:

De blauwe stippen geven weer aan dat de compiler het begrepen heeft. Helaas, ook deze code geeft runtime een foutmelding.

Het onoverzichtelijke van de Delphi aanpak is dat de velden naast de dataset komen te staan.
Ook hier is er weer sprake van late binding. De FieldByName methode gaat in de dataset zoeken naar een veld met de in de parameter meegegeven naam. En als die die niet kan vinden, omdat er weer eens een spelfout in de naam zit, dan krijg je de bewuste foutmelding.
Gelukkig kent Delphi voor velden ook early binding. Door op de dataset component in de designer te dubbelklikken kom je in de fields editor. Het menu daarvan opent na een rechter-muisklik; kies je hier voor Create all fields dan maakt Delphi een verzameling veld componenten aan. Deze componenten gebruik je in je code en nu controleert de compiler of je de namen goed gespeld hebt.

Het onoverzichtelijke van de Delphi aanpak is dat de velden naast de dataset komen te staan. Logisch gesproken zijn de velden properties van de dataset en kunnen ze niet zonder dataset bestaan. De object treeview laat ze ook als dusdanig zien maar in je code is het één lange lijst van componenten. Alleen in de naam kom je nog iets van de relatie tegen. In een wat grotere datamodule waarin ik voor alle datasets velden heb aangemaakt, raak ik dan ook wel eens de weg kwijt. Maar ik heb wel de zekerheid dat alle referenties aan velden door de compiler is gecontroleerd.

Veldnamen in C#
C# en de rest van .NET werkt met data in (XML-)datasets. Het grote verschil met Delphi is dat een dataset data uit meerdere tabellen kan bevatten; de overeenkomst is dat ook deze data early en late bound te benaderen is. In verreweg de meeste .NET voorbeelden gebeurt dit latebound, voor mij was dat de belangrijkste reden is om dit verhaal te schrijven. Neem de volgende code :
Label5.Text =
DataSetRow1.Tables["SomsData"].Rows[0]["AnyTekst"].
ToString();
Een dataset heeft een verzameling tables; de code gebruikt de tabel SomeData. Deze tabel heeft Rows, een verzameling rijen; de code gebruikt de 1e rij. Deze rij heeft een verzameling velden; de code benadert het veld AnyText. Deze code zal prima compileren, maar run-time krijg je een foutmelding:

De foutmelding is wat cryptisch maar na goed kijken blijkt er een spelfout in de naam van de tabel te staan. Na die gecorrigeerd te hebben krijg ik de volgende fout, maar deze melding is al een stuk duidelijker:

Het leukste van dit alles is dat al dat gedoe met die namen helemaal niet nodig is. Een dataset in VS.NET genereer je meestal vanuit de data-adapter. De data-adapter zal de dataset gaan vullen. VS.NET genereert nu een hele class voor de dataset. De class stamt af van de DataSet class en heeft zo alle methodes en properties om er late bound mee te werken. Maar de gegenereerde class bevat ook types die de tabellen en hun rijen beschrijven. Alle tabellen in een dataset zijn via een getypeerde property te benaderen en alle velden in een rij ook via een getypeerde property. De Class viewer van VS.NET biedt een duidelijk overzicht:

Wat je in Delphi nog zelf op moest starten, krijg je van VS.NET kado en het levert een veel overzichtelijker beeld van je data. Als je meer wil weten over verschillen en overeenkomsten tussen de Delphi en de .NET benadering van data, ben je welkom op mijn website. Het artikel www.Gekko-Software.nl/DotNet/Art06.htm is daar helemaal aan gewijd. Als je de informatie uit de getypeerde dataset loslaat op ons voorbeeld, dan doet deze regel code precies hetzelfde als de latebound versie:
Label5.Text = dataSetRow1.SomeData[0].AnyText;
Van de dataset pak ik in de SomeData tabel rij 0 en daarvan het veld AnyText. En de compiler controleert nu de code in plaats van de gebruiker.
Wat kan de compiler nog meer voor je doen ?
In de voorbeelden heb ik laten zien hoe je de compiler kunt gebruiken om de juistheid van je code te controleren. De betere compilers, zoals Delphi en C#, kunnen nog veel meer om de kwaliteit van je code te verbeteren. Tot nu toe hebben we het vooral gehad over niet bestaande variabelen. Maar een wel bestaande variabele kan net zo goed voor ernstige problemen zorgen. Een vers gedeclareerde variable heeft nog geen waarde. Als je hem dan gaat vergelijken met iets anders dan heb je geen idee wat daar uit gaat komen. Voor zover het niet tot een totale crash leidt, als je de properties van een niet geïnitialiseerd object opvraagt, dan leidt dat toch gegarandeerd tot een Access Violation. En bij een aantal classes uit de Delphi VCL gaat een Windows 98 machine gegegarandeerd op blauw. Er zijn manieren om je code tegen dit soort onheil te beschermen. In de eerste plaats moet je controleren of je variabelen zijn geïnitialiseerd. En daarnaast moet je zorgen dat een functie altijd een zinnig resultaat oplevert. Dit zijn dingen die de Delphi compiler prima kan.

Deze functie vergeet beide zaken. De rc variabele wordt gedeclareerd waarna twee van zijn vele mogelijke waarden tot een functieresultaat leiden. De compiler vertaalt de code braaf, met blauwe puntjes, maar geeft daarnaast twee warnings. Dat deze niet voor niets zijn moge duidelijk zijn.
De C# compiler is nog veel strenger.
private int Kwadraat()
{
int rc;
switch(rc)
{
case 1 : return 1 *1;
case 2 : return 2 * 2;
}
}
Bovenstaande code levert een foutmelding op : Use of unassigned local variable 'rc'. In de tweede poging geef ik rc een initiele waarde.
private int Kwadraat()
{
int rc = 3;
switch(rc)
{
case 1 : return 1 *1;
case 2 : return 2 * 2;
}
}
Nu ga je merken dat C# een multi-pass compiler is: de eerdere foutmelding is weg, maar ik krijg er een andere voor in de plaats: not all code paths return a value. En ook dit is een foutmelding. Het lukt je niet om een draaiend programma te krijgen voordat je dit hebt opgelost.
Tot slot
In dit verhaal heb ik willen laten zien dat het verschil tussen een gescripte taal en een echte gecompileerde taal niet altijd even groot is. En ook hoe je Delphi of C# zover kan krijgen dat zij toch potentiële problemen in je source gaan vinden en niet de eindgebruiker. Persoonlijk heb ik veel meer vertrouwen in het systematisch doorlopen van je source door een compiler dan de meest systematische test van je uiteindelijke programma. Je hebt weliswaar prachtige profiler tools om te kijken of elke regel ook daadwerkelijk is uitgevoerd maar je compiler doorloopt per definitie elke regel. En biedt zo een zekerheid dat het met de kwaliteit wel goed zit. Althans: met die van mijn variabelen.