Class_header
rész vezeti be az osztály nevét. Itt a class
kulcsszó helyett a deferred class
vagy az expanded class
kulcsszavak is állhatnak. A késleltetett (deferred) osztály egy nem teljesen implementált absztrakciót ír le, amelyet más osztályok mint örökösei használnak. A nem késleltetett osztályokat nevezzük effektív osztályoknak. Egy kiterjesztett (expanded) osztály egy példánya nem referencia, hanem maga az objektum. (Az alapértelmezés a referencia.) A kettő kizárja egymást, egy osztály nem lehet egyszerre késleltetett és kiterjesztett.
Az Obsolete
rész, ha van, azt jelzi, hogy az osztály egy régebbi változat, amit csak a létező rendszerekkel való kompatibilitás miatt hagytak meg. Az obsolete
kulcsszó után lehet egy stringet írni, ennek egyetlen hatása, hogy bizonyos nyelv-kezelő eszközök az osztály használóinak figyelmeztető üzenetként megjelenítik ezt a szöveget.
A Formal_generics
rész megadása jelzi, hogy az osztály generic lesz. Itt kell felsorolni a formális generic paraméteket, valamint a rájuk vonatkozó esetleges megszorításokat. Az Inheritance
záradékban az öröklődéssel kapcsolatos információk szerepelhetnek. A Creators
rész creation
kulcsszóval kezdődően tartalmazza azon eljárások listáját, amelyeket a felhasználók az osztály direkt példányainak létrehozására használhatnak (ezek a konstruktorok). A Features
záradékban az osztály attribútumait és metódusait sorolhatjuk fel, a rájuk vonatkozó láthatósági szabályokkal. Az Invariant
részben az osztály invariánsa adható meg. (Ld. még helyességbizonyítás.)
(Ld. még absztrakt adattípusok.)
Egy osztályt a jellemzőivel írhatunk le. A jellemzők kétfélék lehetnek: attribútumok, amelyek leírják az egyes példányokban tárolt információt, illetve rutinok, amelyek algoritmust adnak meg. A C
osztály kliensei a C
jellemzőit hívó utasításokon vagy kifejezéseken keresztül alkalmazhatják a C
példányaira.
Egy jellemző neve egy azonosító, vagy egy (prefix vagy infix) operátor. (Az operátorokról bővebben.) Ha a jellemzőkről beszélünk, mindig meg kell különböztetni azokat a jellemzőket, amelyeket ebben az osztályban vezetünk be, azoktól, amelyeket örökölt. (Az öröklődésről bővebben.)
C
osztály szüleitől származó jellemzők - ha vannak ilyenek - a C
öröklött jellemzői.
C
osztály feature részében tekintsünk egy f
feature-t leíró deklarációt. Ha f
öröklött, akkor ez a deklaráció valójában az f
újradeklarálása, amivel az f
-nek új tulajdonságokat adunk a C
-ben. Különben az f
új jellemző, ami közvetlenül a C
-ben jelenik meg. Ekkor C
az eredet-osztálya az f
-nek, ezt úgy is mondjuk, hogy f
a C
-ben lett bevezetve.
Példa:
frozen
kulcsszót - ekkor a leszármazottakban nem megengedett az újradefiniálása.
Egy f
jellemző szignatúrája egy olyan (argumentum típusok, eredmény típus) rendezett pár, ahol a pár mindkét eleme egy-egy típussorozat, amit a következőképpen definiálhatunk:
f
egy rutin, az argumentum típusok rész az fargumentumainak (esetleg üres) sorozata. Ha f
egy attributum, akkor az argumentum típusok rész az üres sorozat;
f
egy attributum vagy egy függvény, az eredmény típus egy egy elemű sorozat, amelyiknek egyetlen eleme az f
típusa. Ha f
egy eljárás, az eredmény típus az üres sorozat.
Egy objektum egy feature-jére az obj.feature
szintaktikával hivatkozhatunk. Ennek az az érdekessége, hogy Eiffel-ben ez mindenképpen kifejezésnek számít, és nem változónak, mint számos más nyelvben. Emiatt nem állhat értékadás bal oldalán, kb. úgy viselkedik, mintha azt írnánk, hogy a+b
. Így ha egy attribútum értékét külső objektumból akarjuk átállítani, akkor erre minden esetben explicit metódust kell bevezetnünk (pl. set_x
).
Az Eiffel osztályokban megengedett az ún. többszörös deklaráció:
ANY
osztály, amelyik minden osztály őse, és tartalmaz egy általános összehasonlító jellemzőt, az is_equal-t
. Az eredeti verzió két objektumot mezőről mezőre összehasonlítva dönti el, hogy megegyeznek-e. Minden osztály felüldefiniálhatja ezt, de ugyanakkor hasznos lehet, ha megmarad az eredeti mezőről mezőre összehasonlító lehetőség is, ezért az ANY
osztályban a következő áll:
frozen
), tehát a fejlesztők erre is támaszkodhatnak.
Az Eiffel nyelv fejlesztői egy speciális, más nyelvekben ritkán alkalmazott láthatósági technikát választottak: ez a szelektív láthatóság. Lényegében azt jelenti, hogy egy osztály minden egyes feature-jére külön megadhatjuk, hogy melyek azok az osztályok, melyek közvetlenül elérhetik azt. Eiffel-ben a láthatóság objektum- és nem osztályszintű, így ha azt akarjuk, hogy az adott osztály egyéb példányai is elérhessenek egy bizonyos feature-t, akkor ezt explicite meg kell adnunk a láthatósági szabályokban.
Tehát egy osztály tervezésekor a jellemzők láthatóságáról mindig az érintett osztály dönt. Ezenkívül ha valamit elérhetővé tettünk egy osztály számára, akkor ezzel ezen osztály összes leszármazottja számára is elérhető lett.
A láthatóságra vonatkozó kívánalmainkat úgy adhatjuk meg, hogy a feature
kulcsszó után kapcsos zárojelek között felsoroljuk azoknak az osztályoknak a nevét, melyek számára az itt következő feature-öket láthatóvá szeretnénk tenni. Két speciális kulcsszót is használhatunk itt: az egyik az ALL
(mindenki számára legyen látható), a másik a NONE
(az adott objektumon kívül senki számára nem látható).
Példa:
Az Eiffel természetesen támogatja absztrakt osztályok és absztrakt műveletek definiálását. Az absztrakt műveletek egyszerűen úgy definiálhatóak, hogy a törzs helyére a deferred
kulcsszót írjuk. Egy osztály absztrakt, ha legalább egy művelete absztrakt. Ezt az osztály definíciójának elején, a class
kulcsszó előtt is jelezni kell a deferred
kulcsszóval. Ebből is látszik, hogy egy absztrakt osztálynak lehetnek effektív, vagyis már megvalósított műveletei is. A leszármazottnak természetesen lehetősége van egy absztrakt művelet megvalósítására.
A késleltetett (deferred) osztály egy nem teljesen implementált absztrakciót ír le, amelyet más osztályok, mint örökösei használnak. A nem késleltetett osztályokat nevezzük effektív osztályoknak. A késleltetett jellemzők megvalósításakor nincs "újra"-definiálás, hiszen eddig még nem adtunk definíciót ehhez a jellemzőhöz, így ezt nem is kell a redefine záradékban felsorolni.
Mikor van szükségünk késleltetett jellemzőkre?
TREE
osztály is egy absztrakciót ír le, ahol a speciális implementációt csak a leszármazottaknál adjuk meg. Itt többnyire csak egy elő- utófeltétel párt, amely a rutin szemantikáját jellemzi, és amit minden implementációnak meg kell őrizni.
Ezért az absztrakt jellemzők használhatóak a rendszer tervezés és analízis eszközeként. Tervezési időben a rendszer felépítésével vagyunk elfoglalva, nem az implementációjával. Ez ténylegesen egy eszköz az analízisre is: hogyan modellezhetjük a valós világ objektumainak egy bizonyos kategóriáját? Jól felhasználhatjuk arra, hogy a modellezendő objektumok szerkezetét és a szemantikáját jobban megértsük. Mindez végülis független a számítógépes implementációtól. A leszármazott megvalósíthat egy vagy több absztrakt jellemzőt.
Példa a TREE
osztályból:
TWO_WAY_TREE
-ben a következő implementációt kaphatja:
ITERATION
osztályai tartalmaznak olyan általános iterációkat, mint pl.:
do_all, do_while
stb. léteznek. A TRAVERSABLE
egy nagyon általános absztrakt osztály, amelyik megköveteli effektív leszármazottaitól, hogy tartalmazzák az alapvető bejárási lépéseket (bejárás kezdete, lépés a következő elemre, stb.). A do_until
-hoz hasonló effektív rutinok bejárási mintákat definiálnak. (Ezek az osztályok csak részben absztraktak.) Ezután az osztály egy leszármazottja, mondjuk egy listán való iterációt választva effektívé teheti a start, forth, off
rutinokat, és ennek egy konkrét leszármazottja adja majd meg a prepare, action, test, wrapup
tényleges változatait.
Természetesen valódi objektumot csak effektív osztályból hozhatunk létre.
inherit
kulcsszó után adhatók meg. A leszármazott az őseinek összes feature-jét elérheti, és adott esetben módosíthatja az öröklődés reláció során.Vagyis minden ősosztályhoz megadhatja, hogy hogyan szeretné az örökölt műveleteket és attribútumokat felhasználni. Az öröklődés záradék a következő részekből állhat:
inherit
kulcsszó után fel kell sorolni azoknak az ősosztályoknak a nevét, amelyektől örökölt jellemzőket módosítani szeretnénk az öröklés során. Ezután kell megadni a kívánt módosításokat. Azok a jellemzők, melyeket nem említünk meg az inherit
záradékban, természetesen változatlanul örökítődnek tovább.
Egy osztálynak vannak olyan jellemzői is, amelyek nem örökítődnek át automatikusan a leszármazottba: ilyen például egy művelet konstruktor mivolta. Természetesen minden rutin részt vesz az öröklődésben, így a konstruktorok is, de a creation
záradékot minden osztályban újra meg kell írni. Hasonlóan nem öröklődik egy osztály kiterjesztett (expanded
) mivolta. Ennek oka, hogy a kiterjesztettség tulajdonképpen csak az objektumok tárolását illetve elérését befolyásolja, az többi jellemzőre nincs hatással. Ezért kiterjesztett osztálynak minden további nélkül lehet nem kiterjesztett leszármazottja. Ha azt szeretnénk, hogy a leszármazott is kiterjesztett legyen, akkor azt explicite jeleznünk kell az osztály definíciójában.
Az öröklődés záradékban, a Rename
részben lehet átnevezni az örökölt feature-öket. Az átnevezés célja lehet például az, hogy a leszármazott osztályban nagyobb kifejezőerővel bíró új nevet adjunk egy feature-nek, vagy hogy többszörös öröklődés esetén megszüntessük a felmerülő névütközéseket. Az átnevezés az ismételt öröklődés kezelésében is nagy hasznot tehet. Ha egy feature nevét a leszármazott megváltoztatja, attól a feature jelentésében még nem történik változás, csupán a leszármazott osztály kódjában másképp fogják hívni.
Példa:
A láthatóság és az öröklődés az Eiffel-ben ortogonális fogalmak. Ez közelebbről azt jelenti, hogy a leszármazott úgy változtathatja az őseitől örökölt feature-ök láthatóságát, ahogy neki tetszik. Például egy csak az osztályon belül látható feature-t mindenki számára publikussá tehet, és fordítva. Mindezt az öröklődés záradék New_exports
részében tehetjük meg. Ezekből is látszik, hogy az Eiffel nyelv nagy szabadságot enged a láthatósági szabályok megváltoztatásában. Egy művelet export státuszának megváltoztatása kellemetlen meglepetéseket okozhat, ha a műveletet polimorf módon használjuk.
Örökölődéskor az alapértelmezés az, hogy amit a leszármazott nem változtat meg explicit módon, annak a láthatóságára az érvényes, ami a szülő osztályban volt.
Példa:
FIXED_STACK
osztály a viselkedésének jellemzőit, az elérhetőségének módjait a STACK
osztálytól örökli. Itt nincs szükség a láthatósági viszonyok módosítására. A belső reprezentációját viszont az ARRAY
osztálytól származtatja. Itt viszont már nem szeretnénk, hogy a leszármazott osztályt az ARRAY
műveletein keresztül is el tudjuk érni, hiszen az inkonzisztenciához vezetne. Ezért az ARRAY
-től örökölt összes jellemzőt láthatatlanná tesszük.
Egy feature felüldefiniálását a Redefine részben jelezhetjük. Ekkor megváltoztathatjuk az adott feature implementációját, átírhatjuk akár a szignatúráját is, valamit elő- és utófeltételét.
Az implementáció megváltoztatása egyszerűen úgy történik, hogy újraírjuk a rutin törzsét. Ha a műveletet egy referencián keresztül hívjuk, akkor a mutatott objektum dinamikus típusa alapján dől el, hogy melyik implementáció hajtódik végre.
A szignatúra megváltoztatására természetesen csak bizonyos szabályok betartása mellett van lehetőség: a új szignatúrában szereplő típusoknak pozícióhelyesen megfelelési kapcsolatban kell állniuk a régi szignatúrában szereplő típusokkal (vagyis egy bizonyos osztály kizárólag annak egy leszármazottjára cserélhető le).
Például tekintsük a LINKED_LIST[T]
osztályt a Data Structure könyvtárból, ami T típusú elemek egyirányú láncolt listáját reprezentálja. Ennek egy attributuma egy hivatkozás a lista első elemére:
LINKED_LIST[T]
osztály egy közvetlen leszármazottja, a TWO_WAY_LIST[T]
, amelyik a kétirányú láncolt listákat valósítja meg. Ennek az első eleme nyilván nem LINKABLE[T]
típusú lesz, hanem át kell definiálni, egy
BI_LINKABLE[T]
a LINKABLE[T]
egy leszármazottja. Ez megfelel az általános szabálynak, ami megköveteli, hogy egy újradeklarálásnál egy típus csak egy olyan másik típusra változtatható, ami megfelel neki. Ebben a példában az átdefiniált jellemző egy attributum. Gyakran előfordul az eljárási argumentumok típusának megváltoztatási igénye is, így pl. ha az előző osztályban lenne egy
BI_LINKABLE[T]
típusú legyen.
Az ilyen esetek olyan gyakoriak, hogy egy speciális mechanizmust vezettek be az Eiffelben a kezelésükre, ez az ún. lehorgonyzott deklaráció: a put
műveletet deklarálhatjuk a LINKED_LIST
osztályban a következőképpen:
lt
típusa ugyanaz, mint a first_element
típusa, így ennek átdefiniálása maga után vonja az lt típusának átdefiniálását is, vagyis az lt
típusát "hozzákötöttük" a first_element
típusához. Ez szemantikailag ekvivalens a szignatúra újradeklarálásával, de nincs szükség explicit átdefiniálásra.
Újradefiniálás és a típusok - az újradefiniálásnál van típusmegszorítás, ahogy észrevehettük: Legyen pl. f
egy jellemző, ahol a szülőbeli jellemző szignatúrája: ((A,B), C). Ha f
-t egy leszármazottban újradeklaráljuk, az új szignatúra meg kell feleljen a réginek, ez első közelítésben azt jelenti, hogy egy típus akkor felel meg egy másiknak, ha a bázisosztálya egy leszármazottja a másikénak. Egy szignatúra akkor felel meg egy másiknak, ha ugyanannyi argumentuma és eredménye van, és minden típusnak az első szignatúrában megfelel a másikban az ő helyén álló. Pl. a ((X,Y),Z) szignatúra megfelel az előzőnek, ha X megfelel az A-nak, Y a B-nek, Z a C-nek. Ez a szabály pl. azt jelenti, hogy egy újradeklarálás nem változtathatja meg az argumentumok számát, és az argumentumok vagy eredmények típusát csak megfelelő típusokkal helyettesítheti.
Megengedett, hogy egy függvényt újradeklaráljunk attributumként, de természetesen csak paraméter nélküli függvényt. Pl. a count
jellemző megadja az elemek számát a Data Structure Libraryban. Ez lehet egy függvény (esetleg deferred), amit egy későbbi implementációban egy attributum helyettesít. De fordítva nem lehet! Miért? Pl. a B osztályban:
a
-t, de nem definiálná újra a set_a
-t, akkor ez a C objektumaira nem lenne alkalmazható.
Ha az ős egy feature-jét frozen
-ként - vagyis az ősökben nem felüldefiniálhatóként - deklarálta, akkor ez a feature természetesen nem vehet részt újradefinálásban.
Az elő- és utófeltételek megváltoztatásakor az előfeltételt gyengítheti (bővítheti) a leszármazott, az utófeltételt viszont csak szűkíteni lehet. Az ősosztálybeli részt nem kell újra megadni, a felüldefiniálás szándékát a require else
, illetve az ensure then
kulcsszavak használatával kell jelezni. Az osztály invariánst is meg lehet változtatni, ebben az esetben is csak szűkítésre van lehetőség, és az ősosztálybeli részt itt sem kell újra megadni. (Ld. még helyességbizonyítás.)
Többszörös öröklődés használatakor előfordulhat az az eset is, hogy több örökölt feature-ből szeretnénk a leszármazottban egyet csinálni, vagyis összekapcsolni (join) ezeket a feature-öket. Ehhez van szükség az öröklődés záradék Undefine
részére. Az undefine
részben tulajdonképpen annyi történik, hogy egy effektív (implementációval rendelkező) feature-t absztrakttá (deferred
) teszünk. Ez a művelet megőrzi a rutin teljes szignatúráját, a hozzá kapcsolt elő- és utófeltételekkel együtt, csupán a törzset veszi le róla. Az összekapcsolt rutinok szignatúrájának meg kell egyezni. Előfeltételeik vagy kapcsolatban, utófeltételeik és kapcsolatban egyesülnek.
Az összekapcsolásnak már a legfelsőbb absztakciós szinten, a már eleve deferred rutinok esetében is fontos szerepe lehet: a különböző szempontú absztrakciók összevonása. Ebben az esetben természetesen nincs szükség undefine
részekre. Ha viszont már effektív rutinokat szeretnénk összekapcsolni, akkor az összevonni kívánt rutinokat minden öröklési ágon ugyanarra a közös névre kell átnevezni, és egy ágat kivéve valamennyi ágon undefine-olni kell ezt a feature-t. A nem undefine-olt ágon fog szerepelni a közös új implementáció.
Példa:
FIXED_LIST
egy fixméretű tárolás, index szerinti eléréssel és forward bejárással. Ekkor gyakran hasznos, ha azokat az öröklött absztrakt rutinokat, amelyek ugyanarra vonatkoznak, egybekapcsoljuk.
Például a CHAIN
osztály, ami a szekvenciális struktúrákat listaként írja le, két absztrakt osztálytól örököl, ahol mindkettőnek van egy item jellemzője, ami a kurzorpozíciónál álló elemet adja vissza:
CHAIN
osztály, amelyik kombinálja a két elvet, örökli mindkét item jellemzőt. Normálisan ezt a névütközést átnevezéssel kellene feloldani, de most voltaképp az a kívánatos, hogy ezt a kettőt összekapcsolja. Általában megadja a lehetőséget erre a join mechanizmus: azonos néven öröklött absztrakt jellemzők eggyéválnak így.
Az ismételt öröklődés problémája többek között az egyik oka annak, amiért az objektum orientált jellemzőket is magukba olvasztó programozási nyelvek nem implementálták a többszörös öröklődést, csak az egyszereset. Egy osztályhalmaz öröklődési gráfja soha nem tartalmazhat kört, de megengedett az Eiffelben közvetlenül is és közvetve is az ismételt öröklődés. Az egynél többször öröklött jellemzőkről a leszármazott osztály döntheti el, hogy hányszor akarja örökölni. A többször, de azonos néven örökölt feature-ökből csak egy példány lesz elérhető a leszármazottban, így ez az eset nem is okoz különösebb problémát. Gondok akkor merülhetnek fel, ha egy örökölt feature valamennyi példányát megtartjuk az átnevezés segítségével. Az 1.ábrán láthatunk egy tipikus példát erre az esetre. 1. ábra 2. ábra
a1
értékadás utáni dinamikus típusa alapján a D osztályban definiált f
-nek kellene hívódnia. De melyiknek? Ezen ellentmondás feloldására vezették be az öröklődési záradék Select
részét. A hasonló, többszörös öröklődésből fakadó esetekben itt kell jelezni azt, hogy a leszármazott (esetünkben a D
osztály) melyik örökölt f
feature-t választja ki arra az esetre, ha egy ősén keresztül hívódna a kérdéses rutin.
Példa:
Érdemes még beszélni az Eiffel osztályhierarchiájáról. A legfelső szinten a GENERAL
nevű osztály van: itt definiálódnak a minden osztályra jellemző feature-ök. Ennek leszármazottja a PLATFORM
, ami a platformfüggő jellemzőket vezeti be (pl. számábrázoláshoz használt méretek). A következő a sorban az ANY
osztály: ezt tekintik minden felhasználói osztály ősének, implicite minden osztály ennek a leszármazottja. Törzse eredetileg csupán egy PLATFORM
-tól való öröklést tartalmaz, így bármikor újraírható, ha az alkalmazásfejlesztés úgy kívánja. A hierarchia legalján a NONE
osztály van: csupán fikciónak tekinthető, hiszen definíció szerint a NONE
minden osztálynak leszármazottja. Legfőbb feladata az, hogy az üres referencia, a Void
típusát képezi.
Ábra:
/ \
GENERAL <- PLATFORM <- ANY <- ... <- NONE
\ /