Design by Contract, Exceptions, Initialisering Objekt-orienterad programmering och design (DIT952) Johannes Åman Pohjola, 2017
Design by Contract Designfilosofi där en metods specifikation betraktas som ett kontrakt mellan anroparen och metoden. Om du lovar att ge mig två positiva heltal, lovar jag att ge tillbaka deras största gemensamma delare.
Design by Contract Förvillkor (precondition): ett predikat som måste gälla för att metoden ska få anropas. Eftervillkor (postcondition): ett predikat som måste gälla efter att metodanropet är fullbordat, givet att förvillkoret gällde innan. (Klass)invariant: ett predikat som måste gälla för att en klass ska vara i ett välformat tillstånd.
Design by Contract Med metodkontrakt får vi: Resonera om korrekthet Abstraktion (jämför Dependency Inversion Principle) Ansvarsfördelning Precis formulering av Liskov Substitution Principle
Korrekthet by Contract En metod är korrekt, dvs uppfyller sin specifikation, om: För varje anrop som uppfyller förvillkoret så resulterar anropet i att eftervillkoret uppfylls, och Om klassinvarianten var sann innan anropet är den också sann efteråt. Möjliggör formell verifiering: att bevisa programmet korrekt. Begränsning: förutsätter att specifikationen är korrekt, dvs motsvarar kundens önskemål.
Live code Date.hashCode()
Abstraktion by Contract Den som anropar en metod ska bara behöva känna till dess specifikation; inte dess implementation. Specifikationen måste alltså vara tillräckligt informativ för att ge anroparen meningsfulla garantier. /** * Computes a number based on x and * y. x and y must be positive * @return a number */ public int gcd(int x, int y) { } /** * Computes the gcd of x and y. * x and y must be positive. * @return the greatest common * divisor of x and y */ public int gcd(int x, int y) { }
Abstraktion by Contract Den här specifikationen är för informativ. Oläslig. Låg abstraktionsnivå. Anroparen behöver inte känna till dessa detaljer. Unmaintainable: måste skrivas om ifall vi byter till en annan gcd-algoritm. /** * Computes the gcd of x and y by * by first computing the prime * factorization of x and saving the * prime factors in an array, then * doing the same for y, and then * iterate through the prime factors * of y for each prime factor of x * to compute their common prime * factors, save those in an array, * and compute the product of those. * x and y must be positive. * @return the greatest common * divisor of x and y */ public int gcd(int x, int y) { }
Abstraktion by Contract Depend upon abstractions, not concretions. En bra specifikation håller en hög abstraktionsnivå. Anroparen kan bortse från detaljer som specifikationen inte tar upp (DIP). Metodens implementation kan ändras utan att specifikationen behöver ändras (OCP)....men för mycket abstraktion riskerar att utelämna information som anroparen behöver känna till.
Live code Bags, Ciphers
Ansvar by Contract Den som anropar en metod ansvarar för att förvillkoret är uppfyllt. T ex: den som gör anropet x/y ansvarar för att y!= 0 Metoden ansvarar för att uppfylla eftervillkoret, samt att inte krascha eller kasta exceptions, om anroparen uppfyllde förvillkoret. T ex: / måste returnera kvoten av dess argument, om det andra argumentet inte är 0.
Ansvar by Contract Om förvillkoret är brutet så är metoden, enligt Design by Contractfilosofin, helt ansvarsfri. Vad som helst kan hända! Något katastrofalt...eller oväntat, eller... alldeles alldeles underbart!
Ansvar by Contract Att vi får göra vad som helst, betyder såklart inte att vi borde. Defensiv programmering: helgardera så att programmet kan hantera oförutsedda omständigheter på ett snyggt sätt. Lita inte på att input utifrån uppfyller förvillkoret kontrollera! Använd exceptions för att tydligt signalera feltillstånd till anroparen. Använd assertions för att verifiera att inget vansinnigt händer internt.
Exceptions Ett exception (undantag) i Java är ett objekt som representerar, och innehåller information om, ett fel som uppstått av en eller annan anledning. Ett exception kan kastas (throw). Ett exception kan fångas (catch). När ett exception inträffar innebär det en form av non-local transfer of control. Koden följer inte den normala strukturen, utan kan hoppa till ett catch-block långt bort från där exception kastas.
Error vs Exception Alla former av fel-objekt är i Java sub-klasser till klassen Throwable, som namnet till trots är en klass och inte ett interface. Throwable Error representerar ett fel som inte går att återhämta sig från, exekveringen ska avslutas. E.g. VirtualMachineError Kan fångas, men bör bara fångas för att avsluta programmet på ett snyggt sätt. Exception representerar fel som bör fångas och hanteras, på någon nivå i programmet. Error RuntimeException Exception
Checked vs Unchecked RuntimeException representerar buggar saker som inte borde inträffa och därför inte borde behöva varnas för. E.g. ArrayIndexOutOfBoundsException, IllegalArgumentException Dessa är unchecked, dvs behöver inte deklareras från metoder. Även Error och dess subklasser är unchecked. Alla andra exceptions är checked de representerar saker som vi förväntar oss kommer att inträffa under normal körning undantagsfall, förvisso, men ändå. Dessa måste vi varna användare för. E.g. FileNotFoundException, SQLException RuntimeException Unchecked exceptions Exception Checked exceptions
Checked exceptions För checked exceptions måste vi deklarera, i metoders signaturer, om de kan komma att kasta exceptions av typen i fråga: public String readfile(string filename) throws FileNotFoundException { } En metod som anropar readfile måste antingen fånga FileNotFoundException, eller själv deklarera att den kan komma att kasta samma exception. Kallas exception propagation
Att fånga exceptions public void appendfile(string filename, String str) throws IOException { try { String contents = readfile(filename); contents += str; writefile(filename, contents); } catch (FileNotFoundException e) { createfile(filename); writefile(filename, str); } } Vi fångar en sorts fel Men kan fortfarande orsaka andra sorters IOException, e.g. om vi inte har permission att skriva.
Dokumentera exceptions Det är kutym att dokumentera vilka exceptions som kan kastas under vilka omständigheter, så anroparen vet vad hen bör fånga.
Null "I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years." Tony Hoare (CS giant)
Defensive null checking public void appendfile(string filename, String str) throws IOException { if (filename!= null) { try { String contents = readfile(filename); if (contents!= null) { contents += str; writefile(filename, contents); } } catch (FileNotFoundException e) { createfile(filename); writefile(filename, str); } } else { throw new IllegalArgumentException( ); } } Kontrollera: Att argument till metoden inte är null. Att metoder som anropas inuti metoden inte returnerar null.
Design för exceptions Vid felaktig hantering ( buggar ), kasta ett unchecked exception. Principen Fail fast, fail hard. Returnera aldrig null från metoder kasta checked exception istället. Utgå inte från att andra följer den principen. Lämna aldrig objekt i ett felaktigt state. Om ett exception inträffar i en mutator, använd finally för att återställa till tidigare tillstånd. Kallas Failure Atomicity (jfr rollback för databas-transaktioner).
Assertions Ett assert-statement gör inget om testet returnerar true. Annars kastas ett AssertionError. Behöver aktiveras med flaggan -ea Implicit dokumentation av invarianter och antaganden. Användbart för debugging. Assert:a saker som borde vara omöjliga att bryta mot. Felaktig input är långt ifrån omöjligt! int z = gcd(int x, int y); assert x % z == 0; assert y % z == 0; public class Rational { private int numerator; private int denominator; } private boolean invariant() { return(denominator!= 0); } public void Foo() { assert invariant(); }
Live code Bags
Liskov Substitution Principle Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. Robert C. Martin If a client thinks he has a reference to an object of type A but actually has a reference to an object of subtype B, there should be no surprises when he sends messages to the object. Principle of least astonishment Dale Skrien
LSP med Design by Contract En formell formulering av Barbara Liskov själv (minus formler): Förvillkor får inte förstärkas i subtypen. Eftervillkor får inte försvagas i subtypen. Subtypen bevarar supertypens invariant. Subtypen möjliggör inga tillståndsförändringar som: vore omöjliga att göra i supertypen, och är observerbara via typernas gemensamma metoder.
LSP med Design by Contract Rectangle är inte en subtyp till Square: invarianten att sidorna är lika långa bevaras inte. MutableSquare är inte en subtyp till MutableRectangle: stärker förvillkoren till metoder som bara ändrar en sida i taget. FIFO-kö är inte en subtyp till FILO-kö: pop()-metoderna har helt olika eftervillkor. Point är inte en subtyp till ImmutablePoint: tillståndsförändringar via move är observerbara via getx() CircleWithMutableRadius är en subtyp till ImmutablePoint: mutation av radien är visserligen omöjlig i supertypen, men sådana mutationer är inte observerbara om vi betraktar cirkeln som en punkt. (Obs: ändå fult: inget is-a-förhållande)
LSP med Design by Contract Följer man LSP så bevaras korrekthet av kovariant typsubstitution! Ett korrekt program förblir korrekt om man byter ut alla förekomster av en typ mot förekomster av en subtyp som följer LSP. (korrekt = följer sitt kontrakt) (förutsätter att klienter beror på specifikationen, inte implementationen)
LSP med Design by Contract Att hålla kontrakt på en hög abstraktionsnivå är att följa OCP! Gör det enklare att definiera subtyper som följer LSP. Subtypens kontrakt kan vara: en konkretisering av supertypens kontrakt, och/eller en utökning av supertypens kontrakt, med starkare eftervillkor och svagare förvillkor.
Live code Bags, Ciphers
Initialisering av objekt När vi anropar en konstruktor sätter vi igång en kedja av händelser, som mynnar ut i att vi får tillbaka ett objekt av typen i fråga. (Detta förutsatt att inga throwables kastas)
Initialisering av objekt 1. Statisk initialisering av klassen ( maskinen startar upp ). static initializer blocks, samt initialisering för static attribut. Görs bara om maskinen inte redan startats av ett tidigare anrop, till konstruktor eller någon static metod (eller användning av static attribut). 2. Anrop till konstruktorn för objektets superklass ( maskinen utgår från tidigare modell ) Explicit anrop till någon super( )-konstruktor måste göras allra först i en konstruktor. Om ingen super-konstruktor anropas explicit, anropas implicit super(). 3. Initialisering av objektet ( maskinen skapar grunden ) Non-static initializer blocks, samt initialisering för non-static attribut. 4. Exekvering av konstruktorn självt ( maskinen färdigställer ) Koden som explicit skrivits i konstruktorn, förutom eventuellt anrop till en super-konstruktor.
Exempel på initialisering public class Init { static String hello = Hello! ; int x = 5; String foo; } Init() { constructorcode(); } Init init = new Init(); ======================== hello = Hello! ; super(); x = 5; foo = null; constructorcode(); Ett anrop av konstruktorn resulterar i 1. static init 2. super 3. object init 4. constructor