Softwarekwaliteit
1    Inleiding

2    Do's and Don'ts

2.1    Objecten
2.1.1      Constructors en destructors
2.1.2      Objecten creëren en opruimen op hetzelfde niveau
2.1.3      Try...Finally.End
2.1.4      Typecasts
2.1.5      Opruimen van forms en andere objecten
2.1.6      Gebruik TObjectList i.p.v. TList

2.2    Methods
2.2.1      Parameters als const meegeven
2.2.2      Check precondities


2.3    Algemeen
2.3.1      Exceptions
2.3.2      Constanten
2.3.3      Scoop van variabelen


3    Aanbevelingen

3.1    Architectuur
3.1.1      Scheiding source code: Generiek - Specifiek
3.1.2      Scheiding source code: Model-View-Controller
3.1.3      Elimineer redundantie
3.1.4      Objectoriëntatie en Interfaces


3.2    Overig
3.2.1      Uses secties
3.2.2      Autocreate forms niet gebruiken
3.2.3      Search path
3.2.4      Source formatter
3.2.5      Third-party libraries
3.2.6      Benut bestaande functionaliteit
3.2.7      Enumerated types


4    Links




1 Inleiding

Vaak zijn problemen met softwarekwaliteit historisch ontstaan (verkeerd aangeleerd). Het zal niet mogelijk zijn om al deze problemen op korte termijn op te lossen. Hoewel nieuwbouw (nieuwe architectuur) vaak de beste kansen biedt, kan dit document ook bruikbaar zijn bij onderhoud aan bestaande source code. De beschreven do's and don'ts hebben vaak weinig impact op bestaande code terwijl het wel de kwaliteit verbeterd. Bovendien zal refactoring van de huidige code een overstap naar een andere omgeving (zoals C#) makkelijker maken.

Aandacht voor softwarekwaliteit, het toepassen van wat in dit document is beschreven, gaat (op lange termijn) niet ten koste van de doorlooptijd. Het zal zelfs tijd besparen doordat er minder code is om te onderhouden, er minder bugs ontstaan en aanpassingen eenvoudiger kunnen worden doorgevoerd.

De inhoud van dit document is gebaseerd op (en soms specifiek voor) Delphi, maar de concepten zullen ook voor andere ontwikkelomgevingen (zoals C#) van toepassing zijn. Dit document is zeker niet compleet. De onderwerpen die aan de orde komen zijn gebaseerd op situaties die ik in de praktijk bij verschillende projecten ben tegengekomen. Correcties of aanvullingen op dit document zijn zeer welkom.

Franck van der Sluijs, februari 2006




2 Do's and Don’ts


2.1 Objecten


2.1.1 Constructors en destructors

Houd je aan de Delphi conventie en noem constructors altijd Create en destructors Destroy. Maak geen andere methods met deze namen, dat is verwarrend. Roep in een constructor en destructor altijd inherited aan, tenzij er een goede reden is om dat niet te doen, ook al weet je dat de base class geen implementatie heeft. Wellicht komt er later een base class tussen en dan is het vaak wel wenselijk dat de implementatie van die constructor en destructor ook wordt uitgevoerd. Maak je eigen constructor en destructor virtual. Het is een goede gewoonte om in de constructor alleen andere objecten te creëren en het initialiseren aan een AfterConstruction method over te laten.


2.1.2 Objecten creëren en opruimen op hetzelfde niveau

Zorg ervoor dat objecten altijd opgeruimd worden, dus ook bij exceptions. Dit betekent dat je objecten altijd op hetzelfde niveau (in dezelfde class) moet opruimen als waar je ze creëert. Voor tijdelijke objecten die in een method gebruikt worden kan hiervoor Try. Finally.End (zie 2.1.3) gebruikt worden. Objecten die in een constructor worden gecreëerd, kunnen het beste (in omgekeerde volgorde) in de destructor worden opgeruimd. Maak bijvoorkeur geen functies die een object creëren en dat als resultaat teruggeven, zoals:

function OphalenMeldingen: TStringList;

Beter is om hier de volgende procedure van te maken:

procedure OphalenMeldingen(const Meldingen: TStringList)

De method OphalenMeldingen is nu geen owner meer van de StringList. De aanroepende code zorgt voor het creëren en het opruimen van de StringList. In dit voorbeeld is het nog beter om er de volgende definitie van te maken:

procedure OphalenMeldingen(const Meldingen: TStrings)

De aanroepende functie kan er nog steeds een StringList instoppen maar dat hoeft niet. Doordat OphalenMeldingen generiek is, kan er evengoed een andere afgeleide van TStrings worden meegegeven. Het type van de parameter moet dus zo goed mogelijk bij de functionaliteit passen, zo specifiek mogelijk, maar niet specifieker dan nodig.

Er kunnen situaties zijn waarbij je toch in een method een object creëert die je beschikbaar wilt stellen aan de aanroepende code. Dit zijn vaak situaties waar Interfaces (zie 3.1.4) goed gebruikt kunnen worden. Garbage collection garandeert dan dat het object wordt opgeruimd.


2.1.3 Try...Finally...End

Gebruik Try...Finally...End op plaatsen waar locale objecten worden gecreëerd. Hiermee wordt gegarandeerd dat objecten altijd netjes worden opgeruimd, zodat er geen memory leaks ontstaan. Het is een goede gewoonte om één Try.Finally..End blok te gebruiken per object. Zonodig kunnen Try.Finally..End blokken genest worden. Naast het opruimen van objecten kan een Try.Finally..End ook goed gebruikt worden voor bijv. het resetten van de cursor nadat deze op hourglass mode is gezet. Degene die Try.Finally..End nog niet kennen zullen het misschien verwarren met de Try.Except..End. Beide hebben te maken met exceptions. Een Finally blok wordt echter juist altijd uitgevoerd, terwijl een Except blok bedoelt is voor het afvangen van (bepaalde) exceptions.


2.1.4 Typecasts

Gebruik waar mogelijk soft typecasts i.p.v. hard typecasts. Een hard typecast (ook wel unsafe typecast genoemd) ziet er als volgt uit:

MyReference := TMyClass(aReference);

Een soft typecast (ook wel een safe typecast genoemd) ziet er zo uit:

MyReference := aReference as TMyClass;

Bij een soft typecast vindt er een controle plaats of de typecast geldig is. Als dat niet zo is zal er een exception optreden met een duidelijke melding. Een hard typecast zal een ongeldige typecast toestaan, maar er zal in dat geval in een later stadium een exception optreden die veel moeilijker correct af te vangen is. In de praktijk kunnen ongeldige typecasts lijden tot onvoorspelbaar gedrag en moeilijk te debuggen foutmeldingen.

De syntax van een soft typecast is ook bruikbaar voor interfaces. Hoewel het resultaat is wat de bedoeling zal zijn, is de werking totaal anders. Bij een soft typecast op een interface zal er automatisch een nieuwe interface opgevraagd worden met QueryInterface. Voor interfaces kan het soms duidelijker zijn om de supports functie (zie Delphi help) te gebruiken.


2.1.5 Opruimen van forms en andere objecten

Hoewel het niet in alle situaties noodzakelijk is, is het beter om je aan te wennen om forms altijd op te ruimen met Release en niet met Free. Overige objecten moeten altijd opgeruimd worden met Free en nooit door het direct aanroepen van Destroy. In plaats van Free kan ook de functie FreeAndNil gebruikt worden. Deze functie zorgt ervoor dat de object reference, na het opruimen van het object, geen ongeldige waarde bevat. FreeAndNil is aan te bevelen als de object reference niet direct out of scope gaat.


2.1.6 Gebruik TObjectList i.p.v. TList

Stop geen objecten in een TList instantie, gebruik daarvoor een TObjectList (of afgeleide) instantie. Omdat een item van TList geen object reference is, is daar alleen een hard typecast mogelijk. Door een objectlist te gebruiken is een soft typecast mogelijk (zie 2.1.4). Bovendien kan een objectlist objecten in de lijst beheren, d.w.z. automatisch opruimen als ze uit de lijst gaan. Default staat dit aan, maar het kan ook uitgezet worden met een parameter van de constructor. Nog mooier is het om een eigen afgeleide van de TObjectList te gebruiken waarin de typecast wordt gedaan. De items van de list zijn dan van je eigen type. Bovendien kunnen aan de eigen afgeleide van TObjectList vaak zinvolle methods worden toegevoegd.


2.2 Methods


2.2.1 Parameters als const meegeven

Default worden parameters in Delphi door call by value doorgegeven. Naast dat dit niet efficiënt is, is het in veel situaties niet wenselijk om in de method de waarde van de parameter aan te passen. Het is daarom beter om parameters waar mogelijk als const doorgeven. Object references kunnen een uitzondering zijn, omdat dat zelf al pointers zijn maakt dat voor de efficiëntie niet uit.


2.2.2 Check precondities

Parameters van methods moeten vaak aan bepaalde voorwaarden voldoen, omdat het anders tot een foutsituatie of exception leidt. Door deze precondities al direct aan het begin van de method te controleren kan veel tijd bespaard worden tijdens het testen en debuggen. Een veel voorkomende test is de controle of een object reference wel geassigned is. Een controle kan er dan als volgt uitzien:

Assert(Assigned(MyReference), 'MyReference not assigned');

Indien aan de conditie niet wordt voldaan zal Assert een exception (EAssertionFailed) genereren. Asserts kunnen met een compiler switch genegeerd worden voor de build van een release versie, maar het is de vraag of dat verstandig is. Asserts zouden altijd actief kunnen zijn, eventueel kunnen voor een release versie de Assertion exceptions afgevangen worden met bijv. een log file. Assert mag alleen gebruikt worden om fouten in de software te onderscheppen. Alle andere foutsituaties zullen met andere controles en eigen exceptions opgevangen moeten worden.


2.3 Algemeen


2.3.1 Exceptions

Als je zelf een exception raised is het een goede gewoonte om daar ook een eigen exception type voor te gebruiken. Op die manier kan de aanroepende code beter op de exception reageren en zullen niet afgehandelde exceptions een duidelijkere foutmelding opleveren. Het beste is er voor te zorgen dat elke exception altijd een keer afgevangen wordt, zodat de gebruiker niet wordt geconfronteerd met een onduidelijke foutmelding. (Bij de afhandeling kan er dan evt. een voor de gebruiker duidelijke melding worden weergegeven.) Als vangnet kan eventueel het OnException event van het Application object gebruikt worden. Dit event gaat af voor elke niet afgehandelde exception. Indien hier geen event handler aan gekoppeld is, wordt er een standaard dialog getoond.


2.3.2 Constanten

Op plaatsen waar een vaste waarde wordt gebruikt, is het verstandig om een constante te gebruiken. Het gebruik van constanten voorkomt typefouten als dezelfde waarde op meerdere plaatsen gebruikt wordt en het aanpassen van de waarde hoeft nog maar op één plaats te gebeuren. Bovendien zorgt het gebruik van constanten voor beter leesbare programmacode. Er bestaan veel standaard constanten in Delphi, bijvoorbeeld voor de keyboard scancodes.


2.3.3 Scoop van variabelen

Beperk de scoop van variabelen zo veel mogelijk. Vermijdt globale variabelen in units en public fields in classes. Maak gebruik van properties met eventuele getters en setters. Delphi maakt (helaas) default voor een form een globale variabele aan. Gebruik deze niet, maar verwijder deze.




3 Aanbevelingen


3.1 Architectuur


3.1.1 Scheiding source code: Generiek - Specifiek

Een scheiding in generieke en specifieke code resulteert in een codebase die beter beheersbaar, herbruikbaar en onderhoudbaar is. (Dit hoeft niet een runtime scheiding (bijv. DLL) te betekenen.) Voor een bedrijf zou er een generieke productonafhankelijke library kunnen worden aangelegd, met hierin eigen toevoegingen op de Delphi library. Daar bovenop kan een library voor een product line gemaakt worden, waarop de producten worden gebaseerd.

Het voordeel is dat de specifieke code niet wordt "bevuild" met generieke code welke voor het product zelf niet relevant is. Een scheiding tussen generieke en specifieke code is noodzakelijk om code herbruikbaar te maken. De totale hoeveelheid code wordt met deze aanpak sterk gereduceerd waardoor ook het aantal bugs afneemt.


3.1.2 Scheiding source code: Model-View-Controller

Naast een scheiding tussen generieke en specifieke source code is een functiescheiding wenselijk. Het Model-View-Controller concept onderscheidt drie basisfuncties: toegang tot data (Model), GUI (View) en de code die de business logica bevat (Controller). Een belangrijk kenmerk van MVC is dat er geen koppeling is tussen Model en View. Vertaalt naar Delphi betekent dat o.a. dat in form units alleen code staat die betrekking heeft op de GUI.


3.1.3 Elimineer redundantie

Programmacode die hetzelfde is (of hetzelfde doet) moet vermeden worden. Het opheffen van redundantie geeft veel winst doordat het onderhoud eenvoudiger is. Als een method veel code bevat, is dat vaak een indicatie dat de code beter kan worden opgesplitst in meerdere methods.


3.1.4 Objectoriëntatie en Interfaces

Objectoriëntatie kan eigenlijk niet los gezien worden, maar hangt samen met paragraaf 3.1.1 en 3.1.2. Het is mogelijk om in Delphi (maar ook in andere objectgeoriënteerde omgevingen) vrij plat (procedureel) te ontwikkelen. Objectoriëntatie biedt echter veel voordelen die de kwaliteit van de software ten goede komen.

Een waardevolle aanvulling kan het gebruik van Interfaces zijn. Omdat de term interface op veel manieren (ook in Delphi) gebruikt wordt, kan dit verwarrend zijn. Hier worden dezelfde Interfaces bedoeld zoals in de context van COM. In Delphi zijn deze Interfaces ook zonder COM toepasbaar. Een objectdefinitie (class) kan in Delphi afgeleid zijn van maar één andere class, terwijl een object wel meerdere Interfaces kan ondersteunen. Met een Interface kan je vastleggen welke public methods en properties een class heeft, zonder dat je de definitie van de class of base class kent. Classes die een zelfde Interface ondersteunen hoeven dus niet van een zelfde base class afgeleid te zijn.


3.2 Overig


3.2.1 Uses secties

Ook al worden ongebruikte units automatisch weg geoptimaliseerd, is het beter om de uses secties zo schoon mogelijk te houden. Dus ongebruikte units niet vermelden. Units met initialisatie code worden namelijk wel meegelinkt en zullen onnodig resources in beslag nemen. Zet de benodigde units in de uses sectie van Implementation (i.p.v. Interface) indien alleen nodig bij implementation. Dit voorkomt circular references bij latere uitbreidingen van de source code.


3.2.2 Autocreate forms niet gebruiken

Gebruik niet de autocreate functie voor forms. Dit zorgt voor onnodig lange opstarttijden (en resource gebruik) van een applicatie omdat alle forms gecreëerd en geïnitialiseerd worden, terwijl niet alle schermen altijd gebruikt zullen worden. Een uitzondering op deze regel kan het mainform zijn. Een andere belangrijke reden om autocreate niet te gebruiken is dat je dan de globale variabele van het form niet meer nodig hebt (zie 2.3.3).


3.2.3 Search path

Misbruik het search path niet. Units die bij een project horen toevoegen aan een project. Library units kunnen via het search path gevonden worden. Een duidelijke directory structuur voor de source code kan hierbij helpen. Alle project files kunnen dan bij elkaar onder één directory (met evt. subdirectories) geplaatst worden. Alle gebruikte units die uit een andere directory (hogere vertakking) komen, horen dus niet bij het project en worden aan het search path toegevoegd. Bij grotere projecten is het verstandig om verschillende subdirectories te maken bijv. voor units die behoren tot model, view en controller (zie 3.1.2).


3.2.4 Source formatter

Het gebruik van een source formatter bespaart tijd en zorgt voor een uniforme lay-out van sourcecode. Er is een goede gratis source formatter voor Delphi beschikbaar (DelForEx). Zorg er wel voor dat de source formatter op de verschillende werkplekken hetzelfde is geconfigureerd (bijv. Borland style). Een source formatter kan wellicht het automatische merge proces iets makkelijker maken, doordat er niet onnodig code wordt vergeleken/gemerged als alleen de style is veranderd.


3.2.5 Third-party libraries

Vermijdt het gebruik (en onderhoud) van eigen oplossingen (functies) die al standaard in Delphi of een third-party library aanwezig zijn. Wel moet worden voorkomen dat er te veel verschillende third-party producten door elkaar gebruikt worden. Wees kritisch bij de selectie en kijk vooral naar de continuïteit. VCL componenten hebben een groter risico dan een functie library. Als er source code meegeleverd wordt, kan de third-party library eventueel zelf gecompileerd worden voor nieuwe Delphi versies. Er zijn goede open source libraries voor Delphi beschikbaar.


3.2.6 Benut bestaande functionaliteit

Dit is een open deur, maar in de praktijk wordt te weinig gebruik gemaakt van beschikbare functionaliteit. Vermijdt het schrijven van programmacode voor zaken die met bijv. alignment van controls, default button's etc. gedaan kunnen worden. Eigen code die dit soort zaken regelt houdt vaak geen rekening met verschillende Windows instellingen (zoals themes en font size). De properties van VCL componenten houden hier wel rekening mee.


3.2.7 Enumerated types

Het gebruik van enumerated types is erg aan te bevelen. Echter het gebruik van de indexen van deze typen kan voor problemen zorgen en dient dus voorkomen te worden. Mocht het gebruik van deze indexen toch wenselijk zijn, dan is het goed om de indexen ook expliciet te definiëren of het enumerated type te vervangen door gewone constanten.




4 Links

  • Jedi Code Library
    De JCL is een goede aanvulling voor Delphi, al is een deel van de JCL overbodig, omdat deze functies inmiddels ook in Delphi beschikbaar zijn. De JCL is een functie library. Er hoeven dus geen componenten geïnstalleerd te worden. De helpfile geeft een goed overzicht van de beschikbare functies.
  • TurboPower
    Verschillende componenten libraries. Er moest vroeger voor betaald worden, maar sinds TurboPower verkocht is, zijn een aantal producten open source geworden.
  • DelForExp
    Een freeware Delphi source formatter waarmee eenvoudig kan worden gezorgd dat alle source code in een zelfde style staat.