Shift Left Coding
Shift left coding Als softwareontwikkelaar heb je er vast wel eens mee te maken gehad: een complex, slecht onderhouden softwaresysteem dat moet worden uitgebreid met nieuwe functionaliteit. Wat ooit begon als een goed ontworpen systeem, is na jarenlange doorontwikkeling verworden tot chaos. Logica uit verschillende logische componenten is met elkaar versmeerd geraakt en er is geen touw meer aan vast te knopen. Niemand weet meer echt hoe het allemaal werkt en tot overmaat van ramp zijn er geen geautomatiseerde tests om nog enigszins te kunnen aantonen dat na jouw aanpassingen alles nog werkt zoals daarvoor. Het is niet voor niets dat moderne software ontwikkelstraten worden ingericht met tal van tools, technieken, processen en meetpunten om fouten – en verval in het algemeen – vroegtijdig te signaleren en te voorkomen. Denk hierbij aan document- en codereviews, statische en dynamische codeanalyse, gebruik van standaarden en (geautomatiseerde) tests. Er is echter nóg een wapen dat kan worden gebruikt in de strijd tegen fouten. Een wapen dat al decennia bestaat en zo simpel en effectief kan worden ingezet dat het haast verplichte kennis is voor iedere ontwikkelaar. Shift left testing ‘Voorkomen is beter dan genezen’ is zo’n beetje het uitgangspunt van wat ook wel shift left testing wordt genoemd. Iedereen die te maken heeft met softwareontwikkeling weet immers dat hoe later een fout wordt gevonden, des te kostbaarder (factoren in zowel tijd en geld) het is om deze weer te verhelpen. Stel je een ontwikkelstraat voor, van links naar rechts. Links is steeds het startpunt voor alle deelproducten die worden ontwikkeld: alle software(componenten) zelf, maar ook bijproducten (artifacts) zoals documentatie, tests, etc. Aan het einde van de straat, uiterst rechts, vind je dan alle ontwikkelde deelproducten die uiteindelijk samen het op te leveren systeem vormen. Dit proces zelf vindt vaak ook in verschillende iteraties plaats, waarbij iteraties in tijd gezien ook van links naar rechts verlopen. Natuurlijk wordt het uiteindelijk opgeleverde systeem grondig getest (met bijv. systeem- en acceptatietests), maar het liefst test je ook bepaalde combinaties van deelproducten (integratietests), de afzonderlijke deelproducten (componenttests) en delen van deelproducten (unittests). Bovendien doe je dit liefst zo vroeg mogelijk in tijd. Ofwel een shift left voor zowel de grootte van te testen onderdelen als het moment van testen. Asserts Iedere ontwikkelaar die wel eens unittests heeft geschreven kent het gebruik van asserts . Dit zijn korte logische expressies die bepaalde condities in de programmacode valideren en altijd TRUE dienen te zijn. Zo niet, dan faalt de test. Ofwel, de code onder test bevat een fout (aangenomen dat de test klopt) en dient te worden gecorrigeerd. Veel minder bekend is het gebruik van asserts buiten de scope van unittests. Ik doel hiermee op asserts in normale programmacode. Dus als een soort sanity checks . En waarom ook niet? Asserts hebben immers als doel codeerfouten te detecteren en dit hoeft niet per se pas te gebeuren in unittests. Let hier op het woordje pas , want een unittest wordt in de praktijk vaak pas geschreven, nadat de code is ontwikkeld. En soms zelfs veel later, bijvoorbeeld om aan bepaalde eisen voor code coverage (percentage code dat wordt afgedekt met tests) te voldoen. Of in het ergste geval helemaal nooit. Daarnaast zie je vaak code coverage niet boven de 80% uitkomen, wat betekent dat minimaal 20% van de code niet wordt afgedekt met tests. Dan is het heel nuttig om asserts op hun plaats te hebben om relevante condities te controleren. Bovendien is het plaatsen van asserts een kleine moeite. Tijdens het coderen weet de ontwikkelaar meestal vrij goed welke programmacondities op een bepaald moment valide horen te zijn en/of welke aannames hij/zij daarover maakt. Ofwel: testen tijdens coderen, een soort shift left coding . Wat betreft het strategisch plaatsen van asserts: hiervoor bieden de richtlijnen van het ‘Design by Contract’ uitkomst. Hierover later meer. Asserts vs exceptions Asserts dienen een ander doel dan exceptions . Asserts hebben als doel codeerfouten te vinden en exceptions zijn er om uitzonderingssituaties door externe factoren te signaleren. Exceptions horen bij de functionele applicatiecode, asserts niet! Sterker nog: asserts worden doorgaans alleen in DEBUG-builds uitgevoerd, dus tijdens ontwikkeling, niet in productiecode! Een veelgehoorde opmerking is dan wat voor zin deze asserts hebben, als ze in productiecode toch niet werken. De crux is dat er in productiecode geen codeerfouten meer horen te zitten (in theorie) en asserts dan alleen valide condities detecteren (wat ook de bedoeling is). Wellicht om deze reden zie je toch vaak exceptions gebruikt worden om codeerfouten te signaleren (bijv. ArgumentException bij invalide methode argument). Maar waarom zou je dit zo doen? Een codeerfout kan zomaar ergens worden afgevangen (try/catch) en onopgemerkt blijven; een assert forceert juist het oplossen van de fout. Een andere reden kan zijn dat ook third party libraries vaak exceptions gebruiken om foutief gebruik te signaleren. Bedenk hierbij dat zij het gebruik van asserts natuurlijk niet kunnen opdringen aan de gebruiker en dat dit productieversies zijn waarin asserts überhaupt niet werken. Bovendien bied je als library een publieke interface aan, waarbij je correct gebruik niet kunt afdwingen, zoals wel het geval is bij interne componenten. Design by Contract (DbC) Een meer methodisch gebruik van asserts vind je bij de techniek genaamd “Design by Contract” (DbC, ook wel bekend als “Code Contracts” of “Contract programming”). Deze techniek heeft zijn oorsprong in de taal Eiffel (ontwikkeld door o.a. Bertrand Meyer), maar is in principe voor iedere object-georiënteerde (OO) taal bruikbaar, zoals C#, Java of C++. (Zie ook: https://www.eiffel.com/values/design-by-contract/introduction/ ) Het uitgangspunt van DbC is dat interactie ( method calls ) tussen softwarecomponenten wordt gespecificeerd door een ‘contract’. Zo’n contract wordt per methode opgesteld (vaak d.m.v. asserts) en beschrijft het volgende:
- Wat verwacht de methode? (precondities, bijv. valide input)
- Wat garandeert de methode? (postcondities, bijv. valide resultaat)
De theorie die hieraan ten grondslag ligt komt voort uit de zgn. ‘Hoare logic’. Dit is een theoretisch model waarbij de ‘Hoare triple’ {P}C{Q} een centrale rol speelt. Deze stelt:
- Als voldaan is aan precondities {P}, dan garandeert de uitvoering van ‘computation’ C (methodelogica) dat aan postcondities {Q} is voldaan, indien de methode een normale return kent.
En dit is in essentie waar het om draait: methoden worden zelf-controlerend door afwijkingen op het contract te detecteren (codeerfouten). Dit is een heel krachtig mechanisme, want deze checks worden tijdens ontwikkeling continue uitgevoerd, waardoor fouten vroegtijdig worden opgemerkt. Ofwel: shift left coding. Kleine kanttekening bij bovenstaande: Exceptions zorgen ervoor dat een methode géén normale return kent en dus ook postcondities niet controleert. Het is aan de ontwikkelaar om ervoor te zorgen dat alleen exceptions worden gegooid voor externe (te verwachten) gebeurtenissen. Exceptions door foutief gebruik van bijv. third party libraries zijn gewoon te voorkomen en gelden om die reden als codeerfout. Wat levert het op? Het toepassen van DbC levert naast het vroegtijdig signaleren van fouten nog een aantal andere voordelen op:
- Methodes worden beter leesbaar en minder complex. In plaats van een serie if (expressie) throw … statements worden simpelere Assert-statements gebruikt, bijv. Assert(expressie, melding). Dit is korter en bondiger en maakt daardoor goed onderscheid tussen pre- en postcondities (het contract) en methodelogica. Ook nodigt het daardoor uit pre- en postcondities te beschrijven.
- Met een Assert signaleert de applicatie de fout en stopt (tijdens ontwikkeling); de fout wordt opgelost. Bij een exception is het zeer de vraag of de fout wordt gesignaleerd of ergens in een catch(…)-putje verdwijnt. En dan nog, met afvangen los je nog steeds de codeerfout niet op, dus waarom zou je?
- Met pre- en postcondities kan de ontwikkelaar expliciet duidelijk maken hoe hij/zij de code bedoeld heeft. Bij een complexe codebase waaraan volop wordt ontwikkeld gedurende langere tijd, kan een methode of class zomaar ergens worden hergebruikt, gekopieerd of worden aangepast. In deze nieuwe context kan het gebeuren dat niet meer wordt voldaan aan het contract en de fout wordt vroegtijdig gedetecteerd.
- Zelfgemaakte conditiechecks (i.p.v. standaard-asserts) kunnen slim worden ingezet om, bijvoorbeeld, te helpen voldoen aan de regels van static code analyzers (bijv. null checks). Of om juist wél in productiecode actief te zijn om eventuele codeerfouten specifiek te loggen.
Hoe te beginnen? Door er gewoon mee te gaan werken en te experimenteren. En dit kan stapsgewijs, begin bijvoorbeeld bij nieuwe code. Bovendien worden dit soort checks niet in productiecode uitgevoerd, dus het risico is te overzien. Wel blijkt het soms lastig te bepalen op welke punten bepaalde condities altijd ‘waar’ dienen te zijn en welke niet. Maar hier goed over nadenken maakt dat je code alleen maar beter wordt. Dus doe er je voordeel mee!