Betere code met C# code contracts

Als software ontwikkelaars, ofwel code kloppers, hebben we een creatief beroep. Ieder probleem kan op meerdere manieren opgelost worden en elke ontwikkelaar brengt zijn eigen inzicht en ervaringen in om tot een optimale implementatie te komen. Ervaring is goed. Maar ervaring is soms ook slecht. Ervaring kan leiden tot routine gedrag en tot onvoldoende nadenken over de code die je net hebt geschreven. Het compileert, het werkt, je bent klaar. Je denkt lang niet altijd goed genoeg na over de aannames die je doet bij het schrijven van een functie. Werkt het meestal, maar soms niet? Bevatten de parameters wel de informatie die je verwacht? Soms moet er met een verse blik naar de code gekeken worden om problemen te voorkomen in plaats van op te lossen. Een code review kan helpen met het voorkomen van runtime errors, maar zou het niet mooi zijn als we onze code expliciter kunnen maken in wat wordt verwacht, wat we aannemen en wat we beloven? Microsoft Code Contracts biedt een hulpmiddel voor het meer voorspelbaar maken van je code. Bovendien zul je merken dat het gebruik maken van dit hulpmiddel helpt met het werpen van een verse blik op je code.

Microsoft Code Contracts biedt een hulpmiddel voor het meer voorspelbaar maken van je code.

Wat is Microsoft Code Contracts?

Microsoft Code Contract is een add-on die je kunt installeren in Visual Studio (het werkt overigens ook via de command line). Deze add-on biedt vervolgens een taalagnostische manier om aannames in je code te formaliseren in een contract. Een contract kan betrekking hebben op precondities, postcondities en “object invariants”. Een contract werkt als documentatie voor je interne en externe API’s. Elke keer dat je je code compileert wordt vervolgens gecontroleerd of je een contract naleeft, of breekt.

Installatie & configuratie

Na installatie van code krijg je een extra configuratie tabblad in je projecten, zie figuur 1. De meeste opties spreken voor zich. Je kunt zowel runtime als static checking aanzetten. Hierover later meer.
Figuur 1: Configuratie opties voor Code Contracts.
 
Door defensieve code toe te voegen lijkt het al een beetje op een contract.

Hoe zien contracts eruit? Precondities onder de loep.

Voordat we in details gaan zullen we even kijken naar hoe een eenvoudig contract met alleen precondities eruit ziet. Neem bijvoorbeeld de volgende functie:
 
public double Delen (int getal, int deler)
{
    return getal / deler;
}

Deze code is functioneel correct, maar we doen de aanname dat de ‘deler’ parameter ongelijk is aan nul. Immers, delen door nul leidt tot een (onverwachte) DivideByZeroExpection. De volgende code is al een hele verbetering, want nu krijgen we in ieder geval een betere foutmelding:
 
public double Delen( int getal, int deler )
{
    if ( deler == 0 )
        throw new ArgumentOutOfRangeException(
            "deler",
            "Deler mag niet nul zijn."
            );

 
    return getal / deler;
}
 
Door defensieve code toe te voegen lijkt het al een beetje op een contract. Het gooien van een exceptie gebeurt echter runtime terwijl we graag meer informatie willen hebben tijdens het compileren van de code. Dit is waar code contracts een oplossing bieden.
 
public double Delen( int getal, int deler )
{
    Contract.Requires( deler != 0 );
 
    return getal / deler;
}
 
Bovenstaande code definieert dat deler nooit nul mag zijn. Als deler wel nul is, dan is er een fout in de aanroepende code. Als we de functie nu toch aanroepen met de waarde nul voor deler, dan volgt een ContractException (het exacte type is System.Diagnostics.Contracts.__ContractsRuntime+ContractException). Wel informatief, maar niet erg fraai. Gelukkig is er een overload beschikbaar.
 
public double Delen( int getal, int deler )
{
    Contract.Requires<ArgumentOutOfRangeException>(
        deler != 
0,
        "Deler mag niet nul zijn."
        );
 
    return getal / deler;
}
 
We hebben nu een contract gedefinieerd waarbij we runtime hetzelfde gedrag hebben alsof we een ArgumentOutOfRangeException gooien, maar die het voordeel biedt dat we nu ook een compile time controle (static checking) kunnen uitvoeren.
 
Het controleren van je code tijdens het compileren van je applicatie is waar zaken interessant worden.

Runtime checking

De code die we hebben toegevoegd kunnen we gebruiken tijdens het uitvoeren van ons programma controles uit te voeren. Doorsnee lijkt me dit wenselijk, maar in sommige, performance kritische toepassingen is dit wellicht onwenselijk en kunnen we leven met de aanname dat de parameters voldoen aan het contract. Als alternatief kun je er voor kiezen om tijdens het testen van je applicatie de runtime checking aan te zetten, maar bij oplevering naar productie een build te maken waarbij de runtime controle uitstaat.

Static checking

Het controleren van je code tijdens het compileren van je applicatie is waar zaken interessant worden.
 
In figuur 1 kun je zien dat ik de optie “Show squigglies” aan heb staan. Dit betekent dat de Code Contracts add-on informatie gaat leveren in m’n code. Figuur 2 laat zien dat als ik een methode toevoeg die de regels van het contract niet naleeft dat ik direct feedback krijgt in de vorm van squigglies (een nieuw woord voor de dikke Van Dale?). Door met de muis over de squigglies te bewegen wordt aanvullende informatie getoond.
 
Figuur 2: Intellisense squigglies maken het leven makkelijk.
 
Behalve de squigglies krijg je overigens ook een warning als je het project compileert.
 
met precondities in een contract kunnen vastleggen wat we verwachten van de ‘buitenwereld’
 
Let op: Als je gebruik maakt van de optie ‘Treat warnings as errors’, dan is het goed om te weten dat de warnings die static checking genereert niet als error gezien worden.

Postcondities

Tot nu hebben we gezien dat we met precondities in een contract kunnen vastleggen wat we verwachten van de ‘buitenwereld’ als ze onze functie willen aanroepen. Met code contracts kunnen we echter ook afspraken maken over het resultaat van een functie.
 
Neem bijvoorbeeld de volgende code:
 
public int RandomDeler()
{
    int g = new Random().Next( 100 );
    return g;
}
 
public double Do()
{
    int g = 10;
    int d = RandomDeler();
 
    return Delen( g, d );
}
 
Deze code levert nog steeds een waarschuwing op bij de aanroep van “Delen”. Er is immers geen garantie dat de functie RandomDeler niet een nul teruggeeft.
We kunnen de code voor RandomDeler verbeteren in:
 
public int RandomDeler()
{
    int g = 0;
    while ( g == 0 )
    {
        g = new Random().Next( 100 );
    }
    return g;
}
 
 
de static checking functionaliteit is slim genoeg om te zien dat die claim niet altijd waar is
 
En nu kan iemand die een code review doet zien dat de deler nooit nul is, maar het is nog niet vastgelegd in een contract, en dus zal de aanroep van “Delen” nog steeds een waarschuwing opleveren. Met behulp van Contract.Ensures kunnen we iets zeggen over het resultaat van de functie:
 
public int RandomDeler()
{
    Contract.Ensures( Contract.Result<int>() != 0 );
 
    int g = new Random().Next( 100 );
    return g;
}
 
Als we bovenstaande method compileren, dan zien wat we wel iets claimen over het resultaat, maar de static checking functionaliteit is slim genoeg om te zien dat die claim niet altijd waar is. Door middel van een waarschuwing bij het compileren, en natuurlijk ook de squigglies zien we dat we niet aan het vastgelegde contract voldoen, zie figuur 3. We krijgen de melding “ensures unproven”, ofwel verzekering onbewezen.
 
Figuur 3: Onbewezen contract.
 
Als we de code verbeteren naar de eerdere fix, dan zitten we op het goede pad.
 
public int RandomDeler()
{
    Contract.Ensures( Contract.Result<int>() != 0 );
 
    int g = 0;
    while ( g == 0 )
    {
        g = new Random().Next( 100 );
    }
    return g;
}
 
Met bovenstaande implementatie van RandomDeler() weet de Do() functie dat RandomDeler() nooit een nul zal opleveren en dus vervalt de waarschuwing bij de aanroep van Delen().

Aannames

In code doe je regelmatig aannames over de inhoud van variabelen. Stel dat in ons eerdere voorbeeld de RandomDeler(0 methode is geimplementeerd door een externe partij en deze heeft geen code contracts geimplementeerd, maar we hebben de documentatie gelezen en er wordt nooit nul geretourneerd. We doen dan de aanname dat dit zo is en we willen de static checker hierover informeren zodat we geen onnodige waarschuwingen krijgen. De Contract.Assume method geeft je de mogelijkheid om je aanname te documenteren in je code.
 
public double Do()
{
    int g = 10;
    // RandomDeler wordt geimplementeerd door third party
    int d = RandomDeler();
 
    Contract.Assume( d != 0, "We nemen aan dat d nooit nul is." );
 
    return Delen( g, d );
}
 
Mocht bij het uitvoeren van de code de aanname onwaar zijn, dan volgt een exception met melding “We nemen aan dat d nooit nul is.”.
 
Soms wil je echter een afspraak vastleggen over de staat van een object.

Object Invariants

Tot nu toe hebben we gekeken naar contracten die iets zeggen over de precondities en postcondities van een functie. Soms wil je echter een afspraak vastleggen over de staat van een object. Stel wat we een punt met X en Y coordinaten hebben, maar het is een bijzonder punt waarbij Y altijd groter moet zijn dan X. Onderstaande code laat een dergelijk class zien.
 
public class BijzonderPunt
{
    public int X { get; set; }
    public int Y { get; set; }
 
    public BijzonderPunt(int x, int y)
    {
        Contract.Requires<ArgumentException>( y > x );
        X = x;
        Y = y;
    }
}
 
Er is echter nog geen contract en hoewel we wel een contract hebben afgesproken over de constructor is er niets wat ons let om de volgende code te schrijven:
 
public BijzonderPunt DoIets()
{
    var bp = new BijzonderPunt(10, 20);
    bp.X = 30;
    return bp;
}
 
Om af te dwingen dat we bij bovenstaande code ook een waarschuwing krijgen kunnen we een Contract Invariant Method toevoegen. Dit een is een private method, die we nooit direct moeten aanroepen, maar die de static checker gebruikt om instantie van de class te valideren. Onze class ziet er nu zo uit:
 
public class BijzonderPunt
{
    public int X { get; set; }
    public int Y { get; set; }
 
    public BijzonderPunt(int x, int y)
    {
        Contract.Requires<ArgumentException>( y > x );
        X = x;
        Y = y;
    }
 
    [ContractInvariantMethod]
    private void ClassContract()
    {
        Contract.Invariant( Y > X );
    }
}
 
Door het toevoegen de ClassContract method, met daarop het ContractInvariantMethod attribuut zal de bp.X = 30; regel een waarschuwing genereren.

Contracts en interfaces

Een interface is een afspraak met de wereld om bepaalde functionaliteit te implementeren. Met een contract kun je een verdieping van die afspraak vastleggen.
 
Stel we hebben de volgende IRandom interface:
 
public interface IRandom
{
    int RandomDeler();
}
 
Wat we eignelijk willen op interface niveau de afspraak vastleggen dat RandomDeler nooit nul retourneerd. Ook hierin heeft code contracts voorzien, en wel middels de ContractClass and ContractClassFor attributen. De code hieronder laat zien hoe het werkt.
 
[ContractClass( typeof( ContractForIRandom ) )]
public interface IRandom
{
    int RandomDeler();
}
 
[ContractClassFor( typeof( IRandom ) )]
public class ContractForIRandom : IRandom
{
 
    #region IRandom Members
 
    public int RandomDeler()
    {
        Contract.Ensures( Contract.Result<int>() != 0 );
        return default( int );
    }
   #endregion
}  
 
public class MijnRandomizer : IRandom
{
 
    #region IRandom Members
 
    public int RandomDeler()
    {
        return 0;
    }
 
    #endregion
}
 
Door op het contract te verwijzen naar een contract class (en vreemd genoeg moet er ook een verwijzing terug zijn) weet de static checker dat er speciale contract regels gelden voor de method RandomDeler(). De implementatie van IRandom door MijnRandomizer zal dan ook een waarschuwing en squigglies genereren omdat het contract gebroken wordt.

Altijd aan of uit?

Code Contracts is iets dat wordt aan- of uitgezet op projectniveau. Maar soms wil je niet alle classes in een project laten controleren. Dit kan door op assemblyniveau de verificatie uit te zetten.
[assembly: Contracts.ContractVerification(false)]
 
Vervolgens kun je het per class of zelfs methode aanzetten door hetzelfde attribuut met de parameter ‘true’ te plaatsen.
 
[ContractVerification( true )]
public double Delen( int getal, int deler )
{
    Contract.Requires<ArgumentOutOfRangeException>(
        deler != 
0,
        "Deler mag niet nul zijn."
        );
 
    return getal / deler;
}
 

Tot slot

Code contracts bieden een elegante manier om parameter checking te implementeren. De code die je hiervoor implementeert kan vervolgens door middel van static checking gebruikt worden om tijdens het compileren van je code al waarschuwingen te krijgen.
Ik heb zelf gemerkt dat door het gebruik van code contracts ik op een meer defensieve manier naar m’n code kijk, niet alleen programmeren voor wat het moet doen, maar ook op voorhand meer naar mogelijke excepties kijken; een soort code review zonder code reviewer.

Links

·         Download Microsoft Code Contracts : http://research.microsoft.com/en-us/projects/contracts/ ( verkort : http://bit.ly/MePem )

 

Geef feedback:

CAPTCHA image
Vul de bovenstaande code hieronder in
Verzend Commentaar