1 Modul 3 - Kontrakt
1.1 Kontraktsprogrammering
Att ett program är korrekt innebär helt enkelt att programmet utför det som specifikationen
kräver. Om ett program inte är korrekt är det fullständigt värdelöst, oavsett hur snyggt
gränssnittet är eller vilken "bra extra funktionalitet" som finns inbyggd.
Tänk dig själv att du som kund går till elektronikaffären för att köpa en DVD. Glatt går du
hem och pluggar in den, sätter i en film och sätter dig tillrätta i soffan. Du upptäcker snabbt
att din nyinköpta DVD inte kan producera något ljud. Hur arg blir du då? Det har ingen
betydelse att DVDn har en fantastiskt snygg display, att den kan visa bilder i jpeg-format o s
v. En viktig funktionalitet som specifikationen för en DVD kräver saknas eller fungerar inte
tillfredsställande.
Här finns alltså två aspekter.
1. All funktionalitet som finns i specifikationen måste finnas med.
2. Funktionaliteten måste vara korrekt.
(I och för sig kan man argumentera på så sätt att såvida en viss funktionalitet beter sig
felaktigt finns ej den specificerade funktionaliteten.)
Hur säkerställer vi att ett program och varje ingående del fungerar som de ska? Det finns olika
metoder för detta.
Formella metoder
Användning av formella metoder är ett sätt att på ett matematiskt sätt bevisa att ett program är
korrekt. Detta är en mycket bra metod, men också tämligen svår att tillämpa, extremt goda
matematiska färdigheter krävs. Då denna metod tillämpas är det vanligen matematiker som
utför bevisföringen medan programmerare implementerar programmet givet vissa
förutsättningar. För de flesta programvaruutvecklare (programmerare) är detta en metod som
är alltför svårtillgänglig. Den är också tidskrävande, varför det i kommersiella syften blir dyrt
att tillämpa metoden.
Det som avgör om metoden ska tillämpas eller inte är beroende av korrekthetskraven på
mjukvaran. I kritiska applikationer som exempelvis mjukvara utvecklad för ett kärnkraftverk
eller ett flygplan kan ingen som helst avvikelse tillåtas eftersom det kan sluta med en enorm
katastrof. Därför används formella metoder.
Givetvis är det ändå ett krav att mjukvaran ska vara korrekt oavsett vad den ska användas till,
emellertid är det inte av enorm betydelse om ett program som exempelvis en mp3-spelare
skulle uppträda på ett oönskat sätt, det uppstår ingen mänsklig katastrof. Därför använder man
vid konstruktion av mjukvara som inte är lika kritisk andra, mer lättillgängliga metoder för att
säkerställa korrekthet. Inga av dem är lika säkra som formella metoder, men det finns ändå
bra metoder såväl som mindre bra. För "normala" applikationer vill vi välja en metod eller en
kombination av metoder som är mycket bra, och ändå rimligt enkla att använda.
Kontrakt
Den metod vi kommer att använda under denna kurs är kontraktsprogrammering. Den
metoden har ni redan fått en introduktion till i första programmeringskursen. I denna kurs
kommer vi att på ett mer systematiskt sätt tillämpa kontrakt vid programmering. Vid all
programmering ska vi tillämpa kontrakt.
Kontrakt innebär i korthet att vi för varje funktion specificerar ett kontrakt. Kontraktet för en
funktion består av ett förvillkor och ett eftervillkor. Förvillkoret anger vad som måste vara
uppfyllt innan anrop till funktionen, ett predikat. Ett predikat är ett uttryck som kan utvärderas
endera till sant eller falskt. Om predikatet (förvillkoret) kan utvärderas till sant är det tillåtet
att anropa funktionen, annars inte. Eftervillkoret anger vad som utförts då funktionen
exekverat färdigt. Såvida klienten uppfyllt förvillkoret garanteras att det som finns
specificerat i eftervillkoret utförts. Om klienten anropar funktionen utan att förvillkoret är
uppfyllt ges inga som helst garantier. Det är klientens ansvar att respektera förvillkoret.
Såvida klienten uppfyller förvillkoret är det leverantörens ansvar att uppfylla eftervillkoret.
Klienten är vem som helst som använder (anropar) funktionen. Leverantören är den som
implementerat funktionen, d v s den programmerare som skrivit funktionen.
Det är leverantören (den som skriver funktionen) som formulerar kontraktet.
Observera att det kan dras många paralleller till vår vardag. Ett exempel, du vill ha mjölk, du
vet att ICA-butiken nära dig tillhandahåller mjölk men endast om du betalar för den. Det finns
ett kontrakt, om än oskrivet. Förvillkoret för att du som klient (kund) ska få mjölk är att du
betalar vad det kostar. Eftervillkoret är att du får mjölk. Uppfyller du förvillkoret garanterar
butiken att eftervillkoret uppfylls. Du är klient, butiken är leverantör. Bryter du, som klient
(kund), mot förvillkoret ges inga garantier alls av butiken. Ponera att du går in och stjäl mjölk.
Vad händer då? Många saker kan hända, du kan få mjölk, du kan bli tagen av polis, få böter,
bli satt i fängelse, eller så blir du kanske bara stoppad men blir av med mjölken. Poängen är
att vi inte vet vad som händer, det är också ointressant eftersom ett kontrakt är till för att
följas. Betala så får du mjölk, passar det inte så finns inget gällande kontrakt.
Vad är då motivationen till att använda kontrakt vid programmering? Det finns flera
anledningar. En av de viktigaste anledningarna är att man har ett designverktyg som gör att
man kan skriva program som är korrekta. Fler positiva effekter är att man får ett tydligt
gränssnitt, en god semantik, tydiga regler för vad som gäller, bra dokumentation, och man
behöver testa minimalt.
Korrekta program
Eftersom programmeraren som skriver funktionen sätter upp ett tydligt kontrakt behöver hon
eller han inte testa en massa saker inne i funktionen. Programmeraren vet av förvillkoret vad
som ska respekteras av den som anropar funktionen. Av eftervillkoret vet programmeraren
exakt vad funktionen ska utföra. Då kan programmeraren implementera den tjänst funktionen
ska utföra med detta som utgångspunkt. Det är en oerhört stor fördel att kunna utgå ifrån ett
förvillkor och ett eftervillkor då man ska skriva funktionen. Man sparar mycket tid och
sannolikheten för att funktionen implementeras korrekt ökar drastiskt
Givet förvillkoret kan programmeraren inrikta sig på att lösa det verkliga problemet, nämligen
hur funktionen ska implementeras för att eftervillkoret ska uppnås.
Kontraktet, d v s både för- och eftervillkor ska skrivas innan man implementerar
funktionen.
Ett tydligt gränssnitt, god semantik, tydliga regler
Gränssnittet för en klass är oftast en samling funktioner/operationer. Varje funktion har ett
gränssnitt som utgörs av dess namn, eventuella parametrar och ett returvärde. För att en
funktions gränssnitt ska vara bra behöver den givetvis ges ett bra och beskrivande namn, det
gäller också för dess parametrar. Detta tillsammans med kontraktet ger ett mycket tydligt
gränssnitt samt en god semantik. Med semantik avses innebörden av något, i detta fall
funktionen. Två viktiga begrepp då man tillämpar vilket språk som helst, och även ett
programspråk, är syntax och semantik.
Syntaxen är de symboler som ingår i språket tillsammans med reglerna för hur symbolerna får
sättas samman för att bilda fraser, algoritmer i vårt fall. Semantiken är innebörden av de fraser
vi satt samman. Det finns ingen kontroll av hur semantiken ska tillämpas, det är upp till oss
som programmerare att upprätthålla en god semantik.
Då man läser ett program ska det vara enkelt att förstå vad som sker. Kontrakten förstärker
semantiken ytterligare utöver en god namngivning av filer, klasser, variabler, konstanter och
funktioner. Kontrakten ger både klienten och leverantören tydliga regler för användandet
respektive implementationen av funktioner.
Kontraktsprogrammering handlar om att sätta upp kontrakt mellan funktionen och den som
anropar funktionen. Funktionen ska om den anropas på rätt sätt ge rätt svar tillbaka.
Exempel 1 Förvillkor för arrayoperation
// Operationen returnerar ett element i en array som lagrar heltal
// Precondition: 0 <= position < size of (array)
// Postcondition: elementet på position "position" har returnerats
int element(int array[], int position)
{
return array[pos];
}
int main()
{
int olle[20];
int pos;
…
cout << ”Mata in position för element. ”;
cin >> pos;
if((pos >= 0) && (pos < 20)) // Kontroll av förvillkor
cout << ”Element på pos ”<<pos << ”=”
<<
element(array, pos);
else
cout <<”Felaktig position!”;
return 0;
}
Exempel 2 Göra undersökning om färger på bilar.
Alt 1.
Fråga person om färg på bil
Svarsalternativ
1. Röd
2. Grön
3. …
4. Svart
5. Har ingen bil – inte någon färg utan svar på annan fråga
Alt 2.
Fråga om personen har bil
Om ja, fråga om färgen på bilen
Svarsalternativ
1. Röd
2. Grön
3. …
4. Svart
Alternativet ”Har ingen bil” behövs inte eftersom man endast frågar personer som har bil om
färgen på bilen. Man tillämpar ett förvillkor. Vad är förvillkoret?
1.2 Invarianter
Kontrakten är också till för att försäkra sig om att vissa grundläggande egenskaper hos datan
som funktionerna opererar på inte förstörs. Reglerna som skyddar dessa grundläggande
egenskaper brukar kallas invarianter.
En invariant ska är ett predikat som alltid ska vara sant. Funktionerna måste upprätthålla
invrianterna.
Exempel 3 Invarianter
Antal personer vid en plats
Invariant: 0 <= antal < antalet personer i världen
Antal barn
Invariant: 0 <= antal < 32 (om man antar fertil ålder 13-45)
Antalet element i begränsad lista
Invariant: 0 <= antal < MAX
Inga funktioner som opererar på en viss datamängd får bryta invarianterna för datamängden.
Vad är invarianterna för datamedlemmarna size och head i en klass Lista?
1.3 Insidan och utsidan
Specifikationer uttryckta med kontrakt
”Utsidan”
”Insidan”
Från ”insidan”
ƒ Vad kräver jag vid anrop av mig (i egenskap av modul/operation)?
ƒ Vad ger jag tillbaka till anroparen?
Från ”utsidan”
ƒ Vad måste jag göra innan jag anropar modulen?
ƒ Vad får jag tillbaka från modulen?
Skillnaden mellan ett kontrakt och att enbart använda för- och eftervillkor är att man även
förbinder sig att hålla villkoren.
Ett kontrakt är således kombinationen för- och eftervillkor plus respekterandet av dessa
villkor
1.4 Förvillkor och eftervillkor, exempel
I klassen List:
//Pre: position >= 1 && position <= size()
//Post: The element at the position "position" has been returned.
int getElement(int position);
//En klient av klassen List
int main()
{
List myList;
myList.add(5);
myList.add(7);
myList.add(33);
int pos = get position from a user;
if(pos >= 1 && pos <= myList.size())
int element = myList.getElement(pos);
else
cout << ”invalid position” << endl;
…
}
1.5 Starka och svaga kontrakt
Nedan ges två olika kontrakt för samma operation. Ett av kontrakten är starkt, ett är svagt.
I klassen List:
Starkt kontrakt
//Pre: position >= 1 && position <= size()
//Post: The element at the position "position" has been returned.
int getElement(int position);
Svagt kontrakt
//Pre: true
//Post: If ( position >= 1 && position <= size() ), the element at
the position "position" has been returned, otherwise -1 has been
returned.
int getElement(int position);
Operationen som har ett starkt kontrakt är bättre. Ett starkt kontrakt betyder att man lägger ett
större ansvar på klienten än vid ett svagt kontrakt.
Om du jämför de båda varianterna ser du att det starkare kontraktet ger en större tydlighet och
bättre semantik. Ett svagt kontrakt innebär alltid ett mer omfattande eftervillkor. Eftervillkoret
måste då ta hänsyn även till den del som är klientens ansvar vid användande av det starka
kontraktet. Det innebär att implementationen blir mer omfattande och att man måste utföra
tester i koden. Det är inte önskvärt!
Vad gäller det svaga kontraktet har vi också en alarmerande problematik vad gäller
semantiken. Såvida positionen inte är OK så returneras -1, det är ju inte bra eftersom -1
mycket väl kan tänkas vara ett giltigt element i listan. Men vad ska vi då returnera om
positionen inte är OK? Det finns inget bra svar, alla heltal kan tänkas ingå som element i
listan.
Såvida det är möjligt ska man därför alltid tillämpa ett starkt kontrakt!
Kännetecknande för svaga kontrakt är att de oftast har true som förvillkor. Detta innebär inte
att alla operationer som har true som förvillkor är svaga i den bemärkelse som nämnts ovan.
Vissa operationer måste ha true som sitt starkaste möjliga förvillkor.
Exempel:
I klassen List:
//Pre: true
//Post: If the list is empty, true has been returned, otherwise
false.
bool isEmpty();
true är det starkaste möjliga förvillkor för denna operation.
Det är inte alltid möjligt att tillämpa starka kontrakt. Alla klientprogrammerare kan antas
kunna respektera starka kontrakt Operationer som däremot anropas av en slutanvändare måste
ha svaga kontrakt. Det går givetvis inte att lita på en slutanvändare som sitter och interagerar
med ett program. Låt oss titta på ett exempel som ni känner igen.
//En klient av klassen List
// Pre: true
// Post: if the user has choosen a valid position in "myList" to
// remove an element from, the element at this position in "myList"
// has been removed, otherwise the user has been informed that he
// should supply a valid position.
void removeElement(List& aList)
{
//Dialog with the user…
int pos = User chooses an element to remove;
if(pos >= 1 && pos <= aList.size()) //Check the
//precondition!
aList.remove(pos);
else
cout << "You have to choose a valid position!"
}
int main()
{
List myList;
switch(anvandarVal)
{
case 1: addElement(myList);
break;
case 2: removeElement(myList);
break;
case 3: search(myList);
//…
}
//…
return 0;
}
ƒ
Som ses ovan måste förstås alla drivrutiner ha svaga kontrakt. Drivrutinerna anropas ju av
en slutanvändare, denne går inte att lita på. Även om du uppmanar användaren att välja en
position som ligger inom ett visst intervall kan du inte vara säker på att detta respekteras.
Givetvis får inte programmet krascha p g a en bångstyrig slutanvändare.
ƒ
En klientprogrammerare däremot som inte följer ett givet kontrakt får skylla sig själv,
denne har gjort sig förtjänt av att skjuta sig i foten.