Szerződés alapú tervezés

A szerződés alapú tervezést (design by contract), mint szoftvertervezési módszert Bertrand Meyer, az Eiffel programozási nyelv megalkotója fejlesztette ki a nyelv részeként az 1980-as közepén. Megközelítése szerint a szoftverek komponenseinek interfészeit formálisan leírt, részletes és ellenőrizhető specifikációkkal együtt kell megtervezni. Ilyen módon egy osztály interfésze kiegészül a metódusokhoz tartozó elő- és utófeltételekkel, valamint az osztályhoz tartozó invariánssal. Ezeket a specifikációkat „szerződéseknek” (contract) nevezi, az üzleti szerződésekben szereplő feltételek analógiájára. Meyer definíciója szerint egy szerződés „egy szoftverelem tulajdonságainak olyan specifikációja, amely befolyásolja, hogy potenciális kliensei hogyan használhatják”.

A szerződést kötő két fél tehát az osztály és az azt használó kliens. Az osztály és kliensei között mintegy „ráutaló magatartásként” jön létre a szerződés, azáltal, hogy a kliens elkezdi használni az osztály által nyújtott szolgáltatásokat. Az osztály a klienseivel való kapcsolatok során vállalja, hogy tartja magát az interfészében definiált utófeltételekhez (és az osztályinvariánshoz), mindaddig, amíg a kliensek tartják magukat az előfeltételekhez. Ebből adódóan világosan elválnak egymástól a felek jogai és kötelességei. A szerződés tehát mindkét félre kötelezettségeket ró: a kliensnek be kell tartania az előfeltételt (az osztálynak pedig megvan a joga, hogy az előfeltételt be nem tartó kliensekkel ne foglalkozzon), és ekkor cserében joga van hasznot húzni az utófeltételből (amelynek a betartása ez esetben persze az osztály kötelessége).

Megjegyzés

Ez a megközelítés hasonló ahhoz, mint amikor buszra szállunk, és jegyünket megváltva automatikusan elfogadjuk az utazási feltételeket, vagyis vállaljuk, hogy azokat betartva utazunk (ez tulajdonképpen előfeltétele a szolgáltatások igénybevételének). A tömegközlekedést szervező cég csak addig vállalja, hogy célba juttatja az utasokat, amíg azok betartják az előfeltételeket. Az előfeltételeket be nem tartó (például késő, vagy túlméretes csomagokkal érkező) utasokról a társaság nem állít semmit .

A szerződés alapú tervezés igen jól használható szoftverkomponenseink alkalmazásprogramozói interfészének (API) tervezésekor, hiszen a szerződéses alapon meghatározott API-k esetében világos, hogy melyik félnek milyen kötelezettségei vannak. Fontos megjegyezni, hogy az elő- és utófeltételek illetve osztályinvariánsok a specifikáció részét alkotják!

A szerződés alapú tervezés alapelvei

Előfeltétel alapelv

Egy metódust meghívó kliensnek a hívás előtt meg kell bizonyosodnia arról, hogy az előfeltétel teljesül.

Utófeltétel alapelv

Egy metódusnak biztosítania kell, hogy amennyiben végrehajtásának kezdetén a hozzá társított előfeltétel teljesült, a végrehajtás végén a kapcsolódó utófeltétel is igaz legyen.

Osztályinvariáns alapelv

Az osztályinvariánsnak az objektum létrehozásától kezdve mindig igaznak kell lennie, minden, a kliensek számára elérhető metódus végrehajtása előtt és futtatása után.

Megjegyzés

Az osztályinvariáns alapelv megfogalmazásából kitűnik, hogy a metódusok végrehajtása közben előfordulhat, hogy az invariáns nem teljesül. Ez természetes, hiszen a metódusokra úgy tekinthetünk, mint amelyek az objektumot egyik állapotából egy másikba viszik át, és persze az átmenet során nem biztosítható mindig az invariáns teljesülése. Mindez persze nem okoz problémát, hiszen a kívülről megfigyelhető állapotokban (vagyis a metódushívások előtt illetve után) az invariánsok teljesülnek.

Példaként tekintsünk egy kiegyensúlyozott bináris keresőfát megvalósító objektumot. Az ezt megvalósító osztály invariánsa (amelynek minden objektum minden megfigyelhető állapotában igaznak kell lennie) az lehet, hogy a fa minden elemének bal és jobb oldali részfáinak magasságkülönbsége legfeljebb 1. Ennek igaznak kell lennie minden műveletvégzés előtt illetve után, azonban a fába történő beszúrást, illetve az abból történő törlést megvalósító metódusok belsejében (törzsében) nem, hiszen ott ideiglenesen előállhat egy kiegyensúlyozatlan állapot. Ezt azonban a beszúrási illetve törlési algoritmus alapján még ezen metódusok belsejében különféle forgatások alkalmazásával ki kell egyensúlyozni annak érdekében, hogy mire a metódusok befejeződnek (és így az objektum újfent kívülről megfigyelhető állapotba kerül), az osztályinvariáns teljesüljön.

Természetesen előfordulhat, hogy egy metódusnak nincs előfeltétele, például ha nincsenek elvárásai a kliensekkel szemben. Ebben az esetben implicit módon azonosan igaz (true) az előfeltétel (ezt értelemszerűen minden kliens mindig teljesíti).

A szerződések leírásához szükséges elemek bizonyos programozási nyelvben a nyelv részét képezik (ilyen nyelv többek között az Eiffel, a D és az Ada 2012), más esetben ezek az eszközök valamilyen szabványos kiegészítésként jelennek meg (ilyen a .NET platformhoz kiadott Code Contracts nevű kiegészítés, amely többek között a C# nyelvű programok számára teszi elérhetővé az eszközöket), míg vannak olyan nyelvek (a C++ és a Java jelenleg ezek közé tartozik), amelyhez különféle, harmadik fél által készített programkönyvtárak biztosítják az elemeket. A Java nyelvhez készített ilyen eszközök közül kiemelendő a Java Modeling Language (JML) nevű interfészspecifikációs nyelv, amely nemcsak a szerződések leírására, de egyebek mellett formális verifikációra is alkalmas, illetve a JML-hez kapcsolódó OpenJML eszköz, azonban ennek bonyolultsága miatt részletesebb bemutatásra inkább egy, a Google által fejlesztett programkönyvtárat, a Contracts for Java-t választottuk, amelyben annotációk segítségével adhatjuk meg a szerződések elemeit.

A szerződés alapú tervezés persze nem váltja ki a tesztelést, leginkább csak mint annak egyféle kiegészítését tekinthetjük, amelynek segítségével hamarabb deríthetjük fel a hibákat. Mindemellett a szerződések betartásának ellenőrzése nyilvánvaló többletterhelést ró egy rendszerre. Fontos azonban látnunk, hogy egy hibamentes program végrehajtása során a szerződések feltételei sosem sérülhetnek, éppen ezért éles környezetben nem is nagyon van szükség az ellenőrzések elvégzésére, elegendő csupán a tesztelés, illetve a belövés során alkalmazni a futásidejű ellenőrzéseket.

Szerződések és az öröklődés

A szerződés alapú tervezés elméletének egyik fontos következménye, hogy jobb megértését biztosítja olyan fontos objektumorientált alapfogalmaknak, mint amilyen az öröklődés, a polimorfizmus, vagy éppen a dinamikus kötés. Meg kell vizsgálnunk, hogy egy szuperosztály–alosztály viszonyban milyen szerep jut az egyes szerződésfajtáknak.

Az első szabály az invariánsokra vonatkozik. Mivel az öröklődés egy „is-egy” (angolul is-a) kapcsolat (például egy Hallgató is egy Személy), amely magával vonja a helyettesíthetőséget is (vagyis ahol a Személy osztály egy objektumára van szükség – például személyek egy kollekciójában –, használható a Hallgató osztály valamely objektuma is), ezért minden olyan megszorításnak, amelyet a szülőosztály példányaira előírtunk, a leszármazottakra is érvényesnek kell lennie. Vagyis egy osztály a szülőjétől nemcsak adattagjait és metódusait, de osztályinvariánsát is örökli!

Annak vizsgálata, hogy mi történik az elő- és utófeltételekkel, a szoftverfejlesztés egy fontos szabályához vezet el. Nyilvánvaló, hogy a leszármazott (a szerződések szempontjából „alvállalkozó”) osztály egyetlen metódusának előfeltétele sem lehet erősebb a szuperosztálybéli metódusáénál, hiszen ha így lenne a helyettesíthetőség nem valósulhatna meg, mivel egy klienst, amely a szuperosztály előfeltételének betartására készül fel, váratlanul érne egy attól erősebb elvárás, amit nem is feltétlenül lenne képes betartani. Hasonló okból nem lehet az alszerződő metódusainak utófeltételét gyengíteni, hiszen a kliensek számítanak az eredeti utófeltétel teljesülésére. Ezek a gondolatok vezetnek el a második szabályhoz: egy felüldefiniált metódus megtarthatja, vagy gyengítheti az előfeltételt, és megtarthatja, vagy erősítheti az utófeltételt!

A feltételek gyengítésére illetve erősítésére az alábbi két logikai törvény adja az elméleti megalapozást:

  1. p ⊃ (p ∨ q)

  2. (p ∧ q) ⊃ p

Contracts for Java (cofoja)

A cofoja elnevezésű ingyenes, nyílt forrású programkönyvtárat (honlapja: http://code.google.com/p/cofoja/) a Google fejlesztette ki, annak érdekében, hogy a szerződéselemeket könnyen olvasható (és könnyen írható) módon lehessen megadni. A cofoja négyféle szerződésfajtát támogat:

2.1. táblázat - A cofoja annotációi

A szerződés típusaAnnotáció (com.google.java.contract.*)Mikor kerül ellenőrzésre?Öröklődés
Invariánsok Invariant A publikus és csomag szintű metódusokba történő belépéskor, illetve publikus és csomag szintű metódusokból és a konstruktorokból való kilépéskoréselve
Előfeltételek Requires Metódusba történő belépéskorvagyolva
Utófeltételek Ensures Metódusból történő szabályos kilépéséselve
Kivételes utófeltételek ThrowEnsures Metódusból történő szabálytalan kilépés (ha kivétel dobódik)éselve

A szerződések megírását megkönnyítendő, két speciális változót is bevezet:

2.2. táblázat - A cofoja pszeudováltozói

KulcsszóMivel használható együtt?PéldaJelentés
old Ensures, ThrowEnsures old(size()) == size() + 1 A kifejezés a metódusba történő belépéskori állapota.
result Ensures result != null A metódus által visszaadott érték. Csak nem-void metódusok esetén használható.

A szintakszis nagyon egyszerű, mindössze a megfelelő szerződést meghatározó annotációt kell alkalmaznunk:

@SzerződésAnnotáció("kifejezés")
SzerződésselEllátottDeklaráció
...

Ha a szerződésben több kifejezést is meg szeretnénk adni (ezek ilyenkor összeéselődnek):

@SzerződésAnnotáció({
    "kifejezés1",
    "kifejezés2",
    ...
})
SzerződésselEllátottDeklaráció
...

Példa:

import com.google.java.contract.*;

@Invariant({
    "osztályinvariáns 1",
    "osztályinvariáns 2"
})
class MyClass {
    @Requires("metódus előfeltétele")
    @Ensures("metódus utófeltétele")
    SomeType someMethod(...) {
        ...
    }

    @Requires({
        "több részes",
        "előfeltétel"
    })
    @ThrowEnsures({ "SomeException", "kivételes utófeltétel" })
    AnotherType anotherMethod(...) {
        ...
    }
}

Eclipse és cofoja

Ahhoz, hogy Eclipse-ben élvezhessük a szerződés alapú fejlesztés előnyeit, néhány beállítást el kell végeznünk. Első lépésként biztosítanunk kell, hogy az Eclipse fejlesztőkörnyezetet egy JDK (ne pedig csak egy JRE) futtassa. Ezt az eclipse.ini konfigurációs állományban a -vm opció értékének kell a JDK bin könyvtárának elérési útját adni, vagyis az alábbi két sort kell megadni, a -vmargs opciót megelőzően:

-vm
<a JDK bin könyvtárának elérési útja>

Példa:

-vm
c:/Program Files/Java/jdk1.7.0_51/bin
-startup
plugins/org.eclipse.equinox.launcher_1.3.0.v20130327-1440.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.200.v20140116-2212
-product
org.eclipse.epp.package.java.product
--launcher.defaultAction
openFile
--launcher.XXMaxPermSize
256M
-showsplash
org.eclipse.platform
--launcher.XXMaxPermSize
256m
--launcher.defaultAction
openFile
--launcher.appendVmargs
-vmargs
-Dosgi.requiredJavaVersion=1.6
-Xms40m
-Xmx512m

A beállítást követően, amennyiben az Eclipse éppen fut, újra kell azt indítani. Emellett be kell szereznünk magát a Contracts for Java osztálykönyvtárat, amelyet a cofoja honlapjáról tölthetünk le (jelen sorok írásakor a legfrissebb változat a cofoja-1.1-r150.jar névre hallgat). Ezt mentsük el, majd adjuk ahhoz a projekthez, amelyben a szerződéseket használni szeretnénk (jobbklikk a projekt nevén → PropertiesJava Build PathLibraries fül → Add JARs... vagy Add External JARs..., attól függően, hogy a munkaterületünkre (workspace) mentettük-e a fenti jar fájlt, avagy sem)! Miután ily módon a classpath-hoz adtuk a fenti jar-t, a com.google.java.contract csomag elemei használhatóak. A cofoja használatát az alábbi egyszerű példán keresztül mutatjuk be:

package hu.unideb.inf.prt.contracts;

import com.google.java.contract.Ensures;
import com.google.java.contract.Requires;
 
public class Contracts {
    public static void main(String[] args) {
        System.out.println(new Numbers().add(-10, 5));
    }
}
 
class Numbers {
    @Requires({ 
    	"c > 0", 
    	"b > 0" 
    })
    @Ensures({ 
    	"result > a", 
    	"result > b" 
    })
    int add(int a, int b) {
        return a - b;
    }
}

Észrevehető, hogy a Numbers osztály add metódusának feltételei nem feltétlenül helyesek (például c változó sehol sem szerepel), de a bemutató szempontjából ez azért nem baj, mert így nyílik lehetőség annak bemutatására, hogy a fejlesztőkörnyezet hogyan hívja fel a figyelmet a hibákra.

A projekt tulajdonságai között (Project Properties) a Java CompilerAnnotation Processing beállítások között mindhárom jelölőnégyzet beállítása után a New... gomb segítségével vigyük fel az alábbi kulcs–érték párokat, mint ahogyan az az Annotációfeldolgozás beállítása ábrán is látható:

2.3. táblázat - Az annotációfeldolgozó számára beállítandó kulcs–érték párok

KulcsÉrték
com.google.java.contract.classoutput %PROJECT.DIR%/bin
com.google.java.contract.classpath %PROJECT.DIR%/lib/cofoja-1.1-r150.jar
com.google.java.contract.sourcepath %PROJECT.DIR%/src


Megjegyzés

Értelemszerűen a fenti változók értékei rendre: a könyvtár, ahová a lefordított állományok kerülnek, a cofoja keretrendszert tartalmazó, korábban letöltött jar fájl, valamint a szerződéseket tartalmazó források könyvtára. A %PROJECT.DIR% változó segítségével mindezeket a projekt könyvtárához képest relatív módon is megadhatjuk.

2.1. ábra - Annotációfeldolgozás beállítása

Annotációfeldolgozás beállítása

Ugyanitt a Factory Path beállításai között szintén be kell állítani a letöltött jar-t, ahogyan az az Annotációfeldolgozót tartalmazó jar beállítása ábrán látható.

2.2. ábra - Annotációfeldolgozót tartalmazó jar beállítása

Annotációfeldolgozót tartalmazó jar beállítása


Egyből meg is kapjuk a c változóról, hogy problémás. Ha ez nem történne meg, akkor a Window menü Preferences menüpontjának kiválasztásával a GeneralWorkspace oldalon be kell állítani a munkaterület automatikus frissítését. Hibák a szerződésekben ábrán látható.

2.3. ábra - Hibák a szerződésekben

Hibák a szerződésekben


Ez azonban csak arra szolgál, hogy az IDE statikusan megjelölje a felderített problémákat, ettől azonban a szerződések futásidejű betartatása nem történik meg, amit könnyen ellenőrizhetünk, ha lefuttatjuk az alkalmazást. Minden további nélküli lefut. A futásidejű ellenőrzéshez ugyanis még egy parancssori virtuálisgép-argumentumot is meg kell adnunk a -javaagent:<jar fájl> opció segítségével. Ezt az Eclipse-ben a Run Configurations... ablak Arguments fülén, a VM arguments szövegmezőben adhatjuk meg. Példa: -javaagent:${project_loc}/lib/cofoja-1.1-r150.jar. A Futásidejű szerződésellenőrzés ábrán látható, hogy a pop művelet üres veremre történő meghívása a size() >= 1 előfeltétel megsértése miatt a PreconditionError hibát okozza.

2.4. ábra - Futásidejű szerződésellenőrzés

Futásidejű szerződésellenőrzés


Példa: verem megvalósítása szerződésekkel

Kiinduló példának tekintsük egy verem meglehetősen egyszerű interfészét, amely két metóduból áll: az egyikkel a verem legfelső elemét ki lehet törölni, míg a másikkal egy új elemet lehet hozzáadni a veremhez.

interface Stack<T> {
  public T pop();
  public void push(T obj);
}

Erre az egyszerű interfészre azonban nem könnyű szerződéseket definiálni. Ez azért van, mert az interfész nem biztosít lekérdező metódust, amely anélkül tenné lehetővé az objektum állapotának vizsgálatát, hogy meg kellene változtatni azt. Annak érdekében, hogy egy objektum állapotára vonatkozóan szerződéseket készítsünk, először is hozzá kell férnünk ehhez az állapothoz. Egy ilyen egyszerű információ lehet a verem aktuális elemszáma. Bővítsük tehát interfészünket egy méretlekérdező metódussal:

interface Stack<T> {
  public int size();
  public T pop();
  public void push(T obj);
}

Bár a Java nyelvben az int típus előjeles egészt reprezentál, nyilvánvló, hogy egy veremben sosem lehet negatív számú elem. Elkészíthetjük hát első szerződésünket, egy osztályinvariánst, mivel ennek az állításnak az osztály minden objektumára minden időpillanatban igaznak kell lennie.

import com.google.java.contract.Invariant;

@Invariant("size() >= 0")
interface Stack<T> {
  public int size();
  public T pop();
  public void push(T obj);
}

Ennél azért többet is ki tudnánk fejezni az eszközrendszer segítségével. Például nyilvánvaló, hogy az üres veremből nem szabad törölni, vagyis a veremből törlés előfeltétele, hogy a veremben legalább egy elem legyen.

import com.google.java.contract.Invariant;
import com.google.java.contract.Requires;

@Invariant("size() >= 0")
interface Stack<T> {
  public int size();

  @Requires("size() >= 1")
  public T pop();

  public void push(T obj);
}

Jól látható, hogy a szerződések segítségével hogyan valósul meg az API-tervezés és dokumentálás, ha arra gondolunk, hogy ez a megoldás egy igen konzervatív szemléletmódot tükröz: a klienseknek biztosítaniuk kell, hogy a verem nem üres, hiszen ez a feltétel a törlés előfeltétele. Ekkor a kliens kódja valahogy így nézhet ki (feltételezve, hogy s egy Stack típusú objektum):

if (s.size() > 0)
  s.pop();

Amennyiben a kliens nem így jár el, és az előfeltétel vizsgálata nélkül végzi a pop metódus hívását, azt kockáztatja, hogy a futásidejű előfeltétel-ellenőrzésen a program elbukik. Azonban az objektum és kliensének viszonya ennél sokkal liberálisabb is lehet, például a pop abban az esetben, ha az üres veremből próbálna meg a kliens elemet kivenni,

  1. null értéket is visszaadhatna,

  2. kiválthatna egy kivételt.

Ezekben az esetekben a kliens nem kényszerülne rá, hogy a pop művelet meghívását a fentebb látható módon őrfeltétellel lássa el. Bármikor meghívható volna a metódus, és a visszatérési érték alapján vagy az alapján, hogy bekövetkezett-e a kivétel, dönthetné el a kliens, hogy a műveletvégzés sikeres volt-e, avagy sem. Az 1. esetben a pop művelet szerződése a következő módon alakulna (a fenti osztály 8-9. sora):

  @Ensures( "size() != 0 || result == null")
  public T pop();

Figyeljük meg, hogy az előfeltétel eltűnt, vagyis ennek a metódusnak ez esetben nincs előfeltétele, viszont megjelent egy utófeltétel, amely tulajdonképpen egy implikációt ír le: abból, hogy a méret 0 (vagyis a verem üres) következik, hogy a visszaadott értéknek (result) null-nak kell lennie. Ennek logikailag ekvivalens átirata a fenti példában látható.

A 2. esetben a pop művelet szerződésének formája a következő lehetne:

  @ThrowEnsures({"EmptyStackException", "size() == 0"})
  public T pop();

Itt a ThrowEnsures használatára láthatunk példát. Itt kivétel–utófeltétel párokat adhatunk meg, tetszőleges számban. Ha az utófeltétel igaz, az adott kivétel kiváltódására számítunk.

Jó lenne azt is leírni, hogy mit tudunk az objektum állapotáról az egyes metódusok lefutását követően. Tudjuk, hogy ha a veremből kiveszünk egy elemet, akkor a verem elemszáma eggyel csökken, míg ha bővítjük a vermet, akkor eggyel több elem lesz benne. Ezek leírására utófeltételeket használhatunk:

import com.google.java.contract.Ensures;
import com.google.java.contract.Invariant;
import com.google.java.contract.Requires;

@Invariant("size() >= 0")
interface Stack<T> {
  public int size();

  @Requires("size() >= 1")
  @Ensures("size() == old(size()) - 1")
  public T pop();

  @Ensures("size() == old(size()) + 1")
  public void push(T obj);
}

Itt a szerződésfajták megadására szolgáló annotációk mellett egy új elemmel, az old kulcsszóval találkozunk, amely egyike azoknak a bővítéseknek, amelyeket a cofoja keretrendszer támogat.

Az old kulcsszó a régi érték vizsgálatát teszi lehetővé. Régi érték alatt az old paramétereként megadott kifejezésnek a metódushívás kezdetén érvényes értékét értjük.

A szerződéseinket tovább erősíthetjük, ha nem csak az elemszámra, de a tartalomra vonatkozóan is előírásokat teszünk. A top művelet legfelső elemhez történő hozzáférést valósítja meg, anélkül, hogy az adott elemet kivennénk a veremből. Az előfeltétel persze ugyanaz, mint a pop esetében.

import com.google.java.contract.Ensures;
import com.google.java.contract.Invariant;
import com.google.java.contract.Requires;

@Invariant("size() >= 0")
interface Stack<T> {
  public int size();

  @Requires("size() >= 1")
  public T top();

  @Requires("size() >= 1")
  @Ensures("size() == old(size()) - 1")
  public T pop();

  @Ensures("size() == old(size()) + 1")
  public void push(T obj);
}

A top művelet segítségével most már lehetőségünk van a push és pop műveletek szerződését olyan módon bővíteni, hogy a tartalomra vonatkozó állítások is megjelenjenek. A pop által visszaadott elemnek (result) ugyanannak kell lennie, amely a pop hívást megelőzően a verem tetején volt, míg a push művelet esetén az utófeltétel egy olyan részfeltétellel bővül, amely azt állítja, hogy a művelet elvégzése után a verem tetején a push által beszúrt elem áll.

import com.google.java.contract.Ensures;
import com.google.java.contract.Invariant;
import com.google.java.contract.Requires;

@Invariant("size() >= 0")
interface Stack<T> {
  public int size();

  @Requires("size() >= 1")
  public T top();

  @Requires("size() >= 1")
  @Ensures({
    "size() == old(size()) - 1",
    "result == old(top())"
  })
  public T pop();

  @Ensures({
    "size() == old(size()) + 1",
    "top() == old(obj)"
  })
  public void push(T obj);
}

A példából az is látható, hogy a cofoja elvárásai szerint hogyan lehet több részfeltételt egyetlen feltételbe gyömöszölni. Értelemszerűen ez nemcsak az utó-, de az előfeltételekre és az invariánsokra is ugyanilyen módon megadható. Az egyetlen annotációban megadott részfeltételek összeéselődnek.

Készítsünk egy, a fenti interfészt megvalósító konkrét osztályt, amely a vermet egy ArrayList becsomagolásával valósítja meg!

import com.google.java.contract.Ensures;
import com.google.java.contract.Invariant;

import java.util.ArrayList;

@Invariant("elements != null")
public class ArrayListStack<T> implements Stack<T> {
  protected ArrayList<T> elements;

  public ArrayListStack() {
    elements = new ArrayList<T>();
  }

  public int size() {
    return elements.size();
  }

  public T top() {
    return elements.get(elements.size() - 1);
  }

  public T pop() {
    return elements.remove(elements.size() - 1);
  }

  @Ensures("elements.contains(old(obj))")
  public void push(T obj) {
    elements.add(obj);
  }
}

Ennek az osztálynak alig van saját jogú szerződése, azonban az általa implementált Stack interfész összes szerződését megörökli. Egy szerződéssel ellátott típus az összes szülőjének (ősosztályának, illetve az általa implementált interfészeknek) minden szerződését megörökli. Ilyen módon a szerződéseket a szuperosztály–alosztály illetve interfész–implementáló osztály viszonyrendszerekben finomítani lehet. Fontos észben tartani, hogy a felüldefiniált metódusokkal törzsével ellentétben a szerződések nem az eredeti helyett, hanem azt kiegészítve értelmezendőek. Invariánsok és utófeltételek esetén az öröklött szerződésből származó és az újonnan megadott feltételek összeéselődnek, míg az előfeltételek esetén vagy művelettel kombinálhatóak össze. Más szóval, egy metódus mindig legalább annyi állapotot és argumentumot fogad el a bemeneten, mint a szülője (vagyis az előfeltételek csak gyengíthetőek), és legalább annyi garanciát nyújtania kell, mint a szülőjének (vagyis az utófeltételek és invariánsok csak erősíthetőek), máskülönben nem volna ahelyett használható (ami ellentmondana a helyettesíthetőség elvének).

Érdemes megjegyezni, hogy az utolsó példában egy protected adattagra hivatkozó feltételeket adtunk meg. Egy szerződés hatásköre megyegyezik azon metódusdeklarációéval, amelyre vonatkozik, épp ezért pontosan azokat az adattagokat éri el és pontosan azokat a metódusokat hívhatja meg. A korlátozott hozzáféréssel rendelkező tagok elérésére tehát van lehetőség, de a szerződéseinket célszerű nyilvánossá tenni, amikor csak lehet, mert így mintegy dokumentációként is szolgálhatnak. Ezen túlmenően, a korlátozott hozzáférésű tagokra történő hivatkozás nemcsak ellenjavalt, de valójában értelme sem nagyon van.