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).
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
Egy metódust meghívó kliensnek a hívás előtt meg kell bizonyosodnia arról, hogy az előfeltétel teljesül.
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.
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.
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.
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:
p ⊃ (p ∨ q)
(p ∧ q) ⊃ p
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ípusa | Annotá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éskor | vagyolva |
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élda | Jelenté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(...) { ... } }
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 →
→ → Libraries 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 → 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 |
É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.
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ó.
Egyből meg is kapjuk a c
változóról,
hogy problémás. Ha ez nem történne meg, akkor a
menü
menüpontjának kiválasztásával
a → oldalon be kell állítani a munkaterület automatikus
frissítését. Hibák a
szerződésekben ábrán látható.
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.
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,
null
értéket is visszaadhatna,
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.