Programmering

iContract: Design efter kontrakt i Java

Ville det ikke være rart, hvis alle Java-klasser, du bruger, inklusive dine egne, lever op til deres løfter? Faktisk ville det ikke være rart, hvis du faktisk vidste nøjagtigt, hvad en given klasse lover? Hvis du er enig, skal du læse videre - Design by Contract og iContract kommer til undsætning.

Bemærk: Kodekilden til eksemplerne i denne artikel kan downloades fra Resources.

Design efter kontrakt

Design by Contract (DBC) softwareudviklingsteknik sikrer software af høj kvalitet ved at garantere, at alle komponenter i et system lever op til dets forventninger. Som udvikler, der bruger DBC, angiver du komponent kontrakter som en del af komponentens interface. Kontrakten specificerer, hvad denne komponent forventer af kunder, og hvad kunder kan forvente af den.

Bertrand Meyer udviklede DBC som en del af hans Eiffel programmeringssprog. Uanset dens oprindelse er DBC en værdifuld designteknik til alle programmeringssprog, inklusive Java.

Central for DBC er forestillingen om en påstand - et boolsk udtryk om et softwaresystems tilstand. Ved kørsel vurderer vi påstandene ved specifikke kontrolpunkter under systemets udførelse. I et gyldigt softwaresystem vurderes alle påstande til sandt. Med andre ord, hvis en påstand vurderes at være falsk, betragter vi softwaresystemet som ugyldigt eller ødelagt.

DBC's centrale forestilling vedrører noget #hævde makro i programmeringssprog C og C ++. DBC tager dog påstanden zillion niveauer yderligere.

I DBC identificerer vi tre forskellige slags udtryk:

  • Forudsætninger
  • Postbetingelser
  • Invarianter

Lad os undersøge hver mere detaljeret.

Forudsætninger

Forudsætninger specificerer betingelser, der skal gælde, før en metode kan udføres. Som sådan evalueres de lige før en metode udføres. Forudsætninger involverer systemtilstanden og argumenterne overført til metoden.

Forudsætninger specificerer forpligtelser, som en klient til en softwarekomponent skal opfylde, før den kan påberåbe sig en bestemt metode til komponenten. Hvis en forudsætning mislykkes, er der en fejl i en softwarekomponents klient.

Postbetingelser

I modsætning hertil angiver postconditions betingelser, der skal holdes, når en metode er afsluttet. Derfor udføres postconditions, når en metode er afsluttet. Efterbetingelser involverer den gamle systemtilstand, den nye systemtilstand, metodeargumenterne og metodens returværdi.

Postconditions specificerer garantier, som en softwarekomponent giver sine kunder. Hvis en posttilstand overtrædes, har softwarekomponenten en fejl.

Invarianter

En invariant specificerer en betingelse, der skal være, når som helst en klient kan påberåbe sig et objekts metode. Invarianter defineres som en del af en klassedefinition. I praksis evalueres invarianter når som helst før og efter en metode på en hvilken som helst klasseinstans, der udføres. En overtrædelse af en invariant kan indikere en fejl i enten klienten eller softwarekomponenten.

Påstande, arv og grænseflader

Alle påstande, der er specificeret for en klasse og dens metoder, gælder også for alle underklasser. Du kan også angive påstande om grænseflader. Som sådan skal alle påstande om en grænseflade holdes for alle klasser, der implementerer grænsefladen.

iContract - DBC med Java

Indtil videre har vi talt om DBC generelt. Du har sandsynligvis nu en idé om, hvad jeg taler om, men hvis du er ny i DBC, kan ting stadig være lidt tåget.

I dette afsnit bliver tingene mere konkrete. iContract, udviklet af Reto Kamer, tilføjer konstruktioner til Java, der giver dig mulighed for at specificere de DBC-påstande, vi talte om tidligere.

Grundlæggende om iContract

iContract er en forprocessor til Java. For at bruge den skal du først behandle din Java-kode med iContract og producere et sæt dekorerede Java-filer. Derefter kompilerer du den dekorerede Java-kode som normalt med Java-kompilatoren.

Alle iContract-direktiver i Java-kode findes i klasse- og metodekommentarer, ligesom Javadoc-direktiver. På denne måde sikrer iContract fuldstændig bagudkompatibilitet med eksisterende Java-kode, og du kan altid direkte kompilere din Java-kode uden iContract-påstandene.

I en typisk programmets livscyklus flytter du dit system fra et udviklingsmiljø til et testmiljø og derefter til et produktionsmiljø. I udviklingsmiljøet vil du instrumentere din kode med iContract-påstande og køre den. På den måde kan du fange nyligt introducerede bugs tidligt. I testmiljøet vil du muligvis stadig beholde hovedparten af ​​påstandene aktiverede, men du bør tage dem ud af præstationskritiske klasser. Nogle gange giver det endda mening at holde nogle påstande aktiveret i et produktionsmiljø, men kun i klasser, der absolut ikke er kritiske for dit systems ydeevne. iContract giver dig mulighed for eksplicit at vælge de klasser, du vil instrumentere med påstande.

Forudsætninger

I iContract placerer du forudsætninger i en metodeoverskrift ved hjælp af @pre direktiv. Her er et eksempel:

/ ** * @pre f> = 0,0 * / offentlig float sqrt (float f) {...} 

Eksemplet forudsætning sikrer, at argumentet f funktion sqrt () er større end eller lig med nul. Kunder, der bruger denne metode, er ansvarlige for at overholde denne forudsætning. Hvis de ikke gør det, vi som implementatorer af sqrt () er simpelthen ikke ansvarlige for konsekvenserne.

Udtrykket efter @pre er et Java Boolean-udtryk.

Postbetingelser

Efterbetingelser føjes ligeledes til headerkommentaren for den metode, de tilhører. I iContract er @stolpe Direktivet definerer postforhold:

/ ** * @pre f> = 0.0 * @post Math.abs ((return * return) - f) <0.001 * / public float sqrt (float f) {...} 

I vores eksempel har vi tilføjet en posttilstand, der sikrer, at sqrt () metode beregner kvadratroden af f inden for en bestemt fejlmargin (+/- 0,001).

iContract introducerer nogle specifikke notationer for postconditions. Først og fremmest, Vend tilbage står for returværdien af ​​metoden. Ved runtime erstattes det af metodens returværdi.

Inden for postcondition er der ofte et behov for at skelne mellem værdien af ​​et argument Før udførelse af metoden og bagefter understøttet i iContract med @pre operatør. Hvis du tilføjer @pre til et udtryk i en posttilstand, evalueres det ud fra systemtilstanden, før metoden udføres:

/ ** * Føj et element til en samling. * * @post c.size () = [email protected] () + 1 * @post c.contains (o) * / public void append (Collection c, Object o) {...} 

I ovenstående kode angiver den første postbetingelse, at størrelsen på samlingen skal vokse med 1, når vi tilføjer et element. Udtrykket c @ pre henviser til samlingen c inden udførelse af Tilføj metode.

Invarianter

Med iContract kan du angive invarianter i headerkommentaren til en klassedefinition:

/ ** * Et positivt heltal er et heltal, der garanteret er positivt. * * @inv intValue ()> 0 * / klasse PositiveInteger udvider Heltal {...} 

I dette eksempel garanterer invarianten, at Positive heltalværdi er altid større end eller lig med nul. Denne påstand kontrolleres før og efter udførelse af en hvilken som helst metode i den klasse.

Objektbegrænsningssprog (OCL)

Selv om påstandsudtrykkene i iContract er gyldige Java-udtryk, er de modelleret efter en delmængde af Object Constraints Language (OCL). OCL er en af ​​de standarder, der opretholdes og koordineres af Object Management Group eller OMG. (OMG tager sig af CORBA og relaterede ting, hvis du går glip af forbindelsen.) OCL var beregnet til at specificere begrænsninger inden for objektmodelleringsværktøjer, der understøtter Unified Modeling Language (UML), en anden standard beskyttet af OMG.

Da iContract-udtryksproget er modelleret efter OCL, giver det nogle avancerede logiske operatorer ud over Java's egne logiske operatorer.

Kvantificeringsmidler: for alt og eksisterer

iContract understøtter for alle og eksisterer kvantifikatorer. Det for alle kvantificering angiver, at en betingelse skal være sand for hvert element i en samling:

/ * * @variant for alle I-medarbejdere e i get-medarbejdere () | * getRooms (). indeholder (e.getOffice ()) * / 

Ovenstående invariant specificerer, at enhver medarbejder, der returneres af getEmployees () har et kontor i samlingen af ​​værelser, der returneres af getRooms (). Bortset fra for alle nøgleord, syntaksen er den samme som en eksisterer udtryk.

Her er et eksempel på brug af eksisterer:

/ ** * @post findes IRoom r i getRooms () | r.isAvailable () * / 

Denne posttilstand specificerer, at samlingen returneres af, når den tilknyttede metode er udført getRooms () vil indeholde mindst et ledigt rum. Det eksisterer fortsætter Java-typen af ​​indsamlingselementet - IR-værelse i eksemplet. r er en variabel, der refererer til ethvert element i samlingen. Det i nøgleord efterfølges af et udtryk, der returnerer en samling (Optælling, Array, eller Kollektion). Dette udtryk efterfølges af en lodret bjælke efterfulgt af en tilstand, der involverer elementvariablen, r i eksemplet. Ansæt eksisterer kvantificering, når en betingelse skal være sand for mindst et element i en samling.

Begge for alle og eksisterer kan anvendes på forskellige typer Java-samlinger. De støtter Optællings, Arrays, og Kollektions.

Implikationer: antyder

iContract leverer indebærer operatør til at specificere begrænsninger for formularen "Hvis A holder, skal B også holde." Vi siger, "A betyder B." Eksempel:

/ ** * @variant getRooms (). isEmpty () indebærer getEmployees (). isEmpty () // ingen værelser, ingen medarbejdere * / 

Denne invariant udtrykker det, når getRooms () samlingen er tom, getEmployees () samlingen skal også være tom. Bemærk, at det ikke angiver, hvornår getEmployees () er tom, getRooms () skal også være tom.

Du kan også kombinere de netop indførte logiske operatorer for at danne komplekse påstande. Eksempel:

/ ** * @variant for alle I-medarbejdere e1 i get-medarbejdere () | * for alle IE-medarbejdere e2 i getEmployees () | * (e1! = e2) indebærer e1.getOffice ()! = e2.getOffice () // et enkelt kontor pr. medarbejder * / 

Begrænsninger, arv og grænseflader

iContract formerer begrænsninger langs arv og interfaceimplementeringsforhold mellem klasser og grænseflader.

Antag klasse B udvider klassen EN. Klasse EN definerer et sæt invarianter, forudsætninger og postbetingelser. I så fald klassens invarianter og forudsætninger EN gælder for klassen B såvel som metoder i klassen B skal opfylde de samme postbetingelser som klassen EN opfylder. Du kan tilføje mere restriktive påstande til klassen B.

Den ovennævnte mekanisme fungerer også til grænseflader og implementeringer. Formode EN og B er grænseflader og klasse C implementerer begge dele. I det tilfælde, C er underlagt invarianter, forudsætninger og efterbetingelser for begge grænseflader, EN og B, såvel som dem, der er defineret direkte i klassen C.

Pas på bivirkninger!

iContract forbedrer kvaliteten af ​​din software ved at give dig mulighed for at fange mange mulige fejl tidligt. Men du kan også skyde dig selv i foden (dvs. introducere nye bugs) ved hjælp af iContract. Det kan ske, når du påberåber dig funktioner i dine iContract-påstande, der fremkalder bivirkninger, der ændrer dit systems tilstand. Det fører til uforudsigelig adfærd, fordi systemet opfører sig anderledes, når du kompilerer din kode uden iContract-instrumentering.

Stakeksemplet

Lad os se på et komplet eksempel. Jeg har defineret Stak interface, der definerer de velkendte operationer i min yndlingsdatastruktur:

/ ** * @inv! isEmpty () indebærer top ()! = null // ingen null-objekter er tilladt * / offentlig grænseflade Stak {/ ** * @pre o! = null * @post! isEmpty () * @post top () == o * / ugyldigt skub (Objekt o); / ** * @pre! isEmpty () * @post @return == top () @ pre * / Object pop (); / ** * @pre! isEmpty () * / Objekt øverst (); boolsk isEmpty (); } 

Vi leverer en enkel implementering af grænsefladen:

importer java.util. *; / ** * @inv isEmpty () indebærer elements.size () == 0 * / offentlig klasse StackImpl implementerer Stack {private final LinkedList elements = new LinkedList (); public void push (Object o) {elements.add (o); } public Object pop () {final Object popped = top (); elements.removeLast (); retur poppet; } public Object top () {return elements.getLast (); } offentlig boolsk isEmpty () {return elements.size () == 0; }} 

Som du kan se, Stak implementering indeholder ikke iContract-påstande. Snarere er alle påstande fremsat i grænsefladen, hvilket betyder, at stakens komponentkontrakt er defineret i grænsefladen i sin helhed. Bare ved at se på Stak interface og dets påstande, Stakadfærd er fuldt specificeret.

Nu tilføjer vi et lille testprogram for at se iContract i aktion:

offentlig klasse StackTest {public static void main (String [] args) {final Stack s = new StackImpl (); s.push ("en"); s.pop (); s.push ("to"); s.push ("tre"); s.pop (); s.pop (); s.pop (); // får en påstand til at mislykkes}} 

Dernæst kører vi iContract for at oprette stakkeeksemplet:

java -cp% CLASSPATH%; src; _contract_db; instr com.reliablesystems.iContract.Tool -Z -a -v -minv, pre, post> -b "javac -classpath% CLASSPATH%; src" -c "javac -classpath % CLASSPATH%; instr "> -n" javac -classpath% CLASSPATH%; _ contract_db; instr "-oinstr / @ p / @ f. @ E -k_contract_db / @ p src / *. Java 

Erklæringen ovenfor garanterer en lille forklaring.