• Nem Talált Eredményt

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

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

A szerződés

4.2.2. 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:

Haladó programnyelvi eszközök 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; sztringbe viszont (néhány később ismertetendő kivétellel) érvényes Java kódot kell írni.

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.

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

import com.google.java.contract.Requires;

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):

8 @Ensures( "size() != 0 || result == null") 9 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ó.

Haladó programnyelvi eszközök

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

8 @ThrowEnsures({"EmptyStackException", "size() == 0"}) 9 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;

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.

Az eddig megalkotott szerződéssel bíró interfész implementálásához még csak veremre sincs szükség, egy sima számláló változó segítségével is képesek lehetünk átverni szerződéseinket, hiszen semmit nem mondtunk az elemekről magukról, csak a verem méretére vonatkozóan írtuk elő feltételeinket. Nem nyilvánvaló azonban, hogy ennél mindenképpen tovább kell-e mennünk, hiszen a szerződés alapú tervezés nem követeli meg, hogy a program teljes specifikációját leírjuk.

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;

Haladó programnyelvi eszközök 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

public class ArrayListStack<T> implements Stack<T> { protected ArrayList<T> elements;

return elements.get(elements.size() - 1);

}

public T pop() {

return elements.remove(elements.size() - 1);

} 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,

Haladó programnyelvi eszközök

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.