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.