Az Eiffel programozási nyelv

Objektum-orientált programozást támogató nyelvi eszközök

Osztályok jellemzői

Az Eiffel programozási nyelvben az objektum-orientált programozást támogató eszközöknek egy nagyon komplex tárházát valósították meg, lényegében az egész nyelv erre van kihegyezve. Az Eiffel tulajdonképpen nem is nevezhető objektum-orientáltnak, inkább az osztály-orientált a helyes kifejezés. Önálló objektumok, osztálytól függetlenül nem hozhatók létre, és az osztályok sem viselkedhetnek objektumként (nem adhatók meg statikus jellegű metódusok). Eiffel-ben az osztály a fordítás egysége is.

Osztályok

Az osztályok azok a komponensek, amelyeket az Eiffel szoftver felépítésénél használnak. Az osztályok a szoftver dekompozíció moduláris egységei, és az Eiffel típusrendszerének alapjai. Típusként tekintve egy osztály leírja objektumok és műveleteik egy halmazát, amelyek a rendszer végrehajtása során létezhetnek. Ez fordítva is igaz, minden objektum, ami a rendszer végrehajtása során létezhet, ezen rendszer valamely osztályának egy példánya. Precízebben: minden objektum pontosan egy osztály direkt példánya, ezt az objektum generáló osztályának nevezzük, de az öröklődési lehetőségek miatt lehet példánya egynél több osztálynak is.

Modulként tekintve egy osztály bevezeti jellemzők (feature) egy halmazát. Bizonyos jellemzők - ezeket hívjuk attribútumoknak - az osztály direkt példányainak mezőit reprezentálják, mások - ezeket rutinoknak hívjuk - az ezen példányokra alkalmazható műveletek.

Az Eiffelben nincs más modularizáló eszköz az osztályon kívül, így egy szoftver rendszer felépítése itt azt jelenti, hogy analizáljuk, hogy a rendszer milyen típusú objektumokkal fog dolgozni, és mindezen típusokra készítünk egy-egy osztályt. Bizonyos esetekben az osztály csak olyan szempontból érdekes, mint egy modul, ami számos eljárást összegyűjt. Ekkor nincs is feltétlenül attribútuma. A rendszer egy ilyen osztálynak nem a direkt példányait hozza létre, hanem más osztályok fogják majd a jellemzőit az öröklődés segítségével használni.

Egy osztály szövege tartalmazza az osztály nevét - amit esetleg formális generic paraméterek követnek, és egyéb, opcionális részeket, melyeket különböző kulcsszavak vezetnek be:

Class_declaration: [Indexing] Class_header [Formal_generics] [Obsolete] [Inheritance] [Creators] [Features] [Invariant] end ["--" class Class_name]


Az Indexing indexelő rész indexelési információkat rendel az osztályhoz, hogy a környezet által támogatott archiválási és visszakeresési eszközök ezeket használhassák. Ezzel most nem foglalkozunk részletesebben. A 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.)

Az osztályok jellemzői

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

  • A C osztály szüleitől származó jellemzők - ha vannak ilyenek - a C öröklött jellemzői.
  • A 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:

    class LINKED_LIST[T] inherit LIST[T] redefine first, start, return end feature --Number of elements count:INTEGER; --Number of items in the list feature -- special elements first:T is --Item at first position require not_empty: not empty do Result:=first_element.item end; --first ...... feature -- cursor movement ...... feature{LINKED_LIST} --chaining previous, next:like first_element; make_sublist..... ....... feature{NONE} --representation first_element:LINKABLE[like first]; --first linkable element put_linkable_left(new:like first_element} is -- insert new to the left of cursor position require empty_if_before: before implies empty; new_exists:new/=Void do ............ ensure count=old count+1; (position/=1)implies(position=old position+1); previous=new end; --put_linkable_left ............... invariant empty =( first_element=Void) .......... end -- class LINKED_LIST


    Egy attribútum bevezetésével egy mezőt specifikálunk, s az osztály minden objektumának lesz egy ilyen mezője. Az attributumok lehetnek konstansok - ezek az osztályleírásban a deklarációban jelennek meg - vagy változók - ezeket kötelező minden objektum példányban tárolni.

    Egy rutin lehet eljárás vagy függvény. Az eljárás nem ad vissza eredményt, műveleteket hajt végre, s ezáltal megváltoztathatja az objektum állapotát. A függvény értéket ad vissza, eközben műveleteket is hajthat végre, de nem változtathatja az objektum állapotát.

    A jellemző deklarációja tartalmazhatja a 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:

  • ha 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;
  • ha 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).

    Többszörös deklaráció

    Az Eiffel osztályokban megengedett az ún. többszörös deklaráció:

    f1, f2, ... fn dekl_törzs


    Ez ugyanaz, mintha azt írtunk volna, hogy

    f1 dekl_törzs; f2 dekl_törzs; ... fn dekl_törzs


    (Kivéve az ún. unique deklaráció, ahogy majd később látjuk.)

    Ez azt jelenti, hogy ezek független jellemzők általában, és a leszármazottaknál bármelyik átnevezhető vagy újradeklarálható. Lényegileg kezdetben szinonímák keletkeznek, de számítunk a későbbi újradeklarálásra, úgy, hogy az eredeti verziót is szeretnénk megtartani.

    Egy példa erre az 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:

    is_equal, frozen standard_is_equal (x:like Current) : BOOLEAN is...


    Itt a második függvény nem újradefiniálható (frozen), tehát a fejlesztők erre is támaszkodhatnak.

    Szelektív láthatóság

    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:

    class C feature {ALL} x : T; -- minden osztály láthatja (az ALL kulcsszó elhagyható) feature {C} y : U; -- a C osztály példányai láthatják feature {NONE} z : V; -- csak az adott objektumon belül látható end -- class C


    Absztrakt (késleltetett) osztályok és jellemzők

    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?

  • Ha egy absztrakt jellemzőt akarunk leírni, ami több lehetséges implementációt lefed.
  • Ha az osztályhierarchia ezen szintjén az absztrakt jellemzőket akarjuk összefogni, függetlenül attól, hogy van-e elég információnk az implementáció megadására.
    Például a 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:

    child_put(v:like item) is .... require not_child_off: not child_off deferred ensure child.item=v end -- child_put


    Ez a rutin a TWO_WAY_TREE-ben a következő implementációt kaphatja:

    child_put(v:like item) is .... require else is_leaf_or_not_off: (not is_leaf)=>(not child_off) local node:like parent do if is_leaf then !!node.make(v); put_child(node); child_start else child_put(v) end ensure then child_item=v end -- child_put


    Vegyük észre az elő- és utófeltétel záradékok új formáját! Az effektív változat előfeltétele logikai vagy kapcsolatban van az eredeti absztrakt előfeltétellel, az utófeltétel pedig logikai és-t jelez. Gyakori, hogy a megvalósítás két vagy több lépésben történik. Például az Iteration Library ITERATION osztályai tartalmaznak olyan általános iterációkat, mint pl.:

    do_until(s:TRAVERSABLE[T]) is -- az s elejétől kezdve alkalmazza az action-t az s minden elemére -- amíg a van és amíg a a teszt igaz require traversable_ exists: s/=Void; invariant_satisfied: invariant_value(s) do from start(s); prepare(s) invariant invariant_value(s) until off(s) or else test(s) loop action(s); forth(s) end; if not off(s) then action(s) end; wrapup(s) ensure not off implies test(s) end -- do_until


    Ehhez hasonlóan 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.

    Öröklődés

    Az öröklődés fontos szerepet játszik az Eiffel módszertanban. A nyelv nagyon fejlett mechanizmussal rendelkezik ennek kezelésére, amihez hasonlót más nyelvekben csak ritkán találhatunk meg. A nyelv támogatja az öröklődés használatát mind altípusképzés, mind kódújrafelhasználás céljából, és ezt a kettőt nem különbözteti meg élesen. Lehetőség van többszörös öröklődés használatára, és az ismételt öröklődés problémáját is jól tudjuk kezelni. A metódusok alapértelmezés szerint virtuálisak. A fejlett öröklődés kezelés miatt Eiffel-ben rendszerint többet használják az öröklődést, mint más nyelvekben.

    Eiffel-ben az öröklődési kapcsolatok az osztály definíciójában az 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:

    Inheritance: inherit Parent_list Parent_list: [Parent ";" ...] Parent: Class_type [Feature_adaption] Feature_adaption: [Rename] [New_exports] [Undefine] [Redefine] [Select] end


    Ez azt jelenti, hogy az 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.

    Átnevezés

    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:

    class COLORED_FRUITS inherit COLORS rename orange as orange_color, red as red_color, black as white, white as black end; FRUITS rename orange_fruit as orange end feature red: INTEGER; end -- class COLORED_FRUITS


    A láthatóság megváltoztatása

    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:

    class FIXED_STACK[T] inherit STACK[T] -- .. ARRAY[T] rename put as array_put, item as array_item export {NONE} all; end -- .. end -- class FIXED_STACK


    Ez egy tipikus módja a többszörös öröklődés használatának Eiffel-ben. Az öröklődés egyik ágán a specifikációs jellemzőket, a másik ágon pedig a reprezentációt örökítjük. Jelen esetben a 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.

    Feature-ök felüldefiniálása

    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:

    first_element: LINKABLE[T]


    A megfelelő objektumok típusa reprezentálja a lista elemeit, egy érték-mutató pár segítségével. A 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

    first_element: BI_LINKABLE[T]


    formára, ahol a 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

    put_element(lt: LINKABLE[T]; i:INTEGER)


    művelet deklarálva, akkor a kétirányú listák osztályának ezt is át kell definiálni, hogy az első argumentum 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:

    put_element(lt: like first_element; i:INTEGER)


    Ez azt jelenti, hogy az 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: INTEGER; set_a is do a:=0 end


    Ekkor ha a C a B egy örököseként újradefiniálhatná az 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.)

    Feature-ök összekapcsolása

    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:

    class D inherit A rename g as f undefine f end; inherit B rename h as f end; inherit C rename i as f undefine f end feature f : T is ... end -- class D


    A második példa a Data Structure Library három hierarchiáján alapul:

  • A tárolási hierarchia az adatszerkezetek tárolási tulajdonságait írja le (fix vagy változó méretű stb.).
  • Az elérési hierarchia azokat a műveleteket fogja össze, amelyeken keresztül elérhetőek és tárolhatóak az elemek (LIFO a veremben, kulcs szerint egy hash-táblában stb.).

  • A bejárási hierarchia az adatszerkezetek bejárási módjait fogja össze (forward, backward, postorder, preorder, stb).


  • Egy adott osztályt a könyvtárban általában többszörös örökléssel kaphatunk meg, pl. a 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:

    deferred class CHAIN[T] inherit BIDIRECTIONAL[T] ..... van egy item deferred rutinja, a bejárási hierarchiából, ez mindig a bejárás során az aktuális elem értéke ACTIVE[T] ..... van egy item deferred rutinja, az elérési hierarchiából, ez a cursor pozíciónál lévő elem ...... end


    A 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ájának kezelése

    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.

    többszörös ismételt

    1. ábra 2. ábra

    a1 : A; d1 : D; ... a1 := d1; a1.f;


    Mi történik ilyenkor a fenti kódrészlet esetében? Az 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:

    class A feature f : T is ... end -- class A class B inherit A redefine f end feature f : T is ... end -- class B class C inherit A redefine f end feature f : T is ... end -- class C class D inherit B rename f as bf select bf end; inherit C rename f as cf end end -- class D


    A 2. ábrán látható, egyszintes ismételt öröklődésnek is van értelme Eiffel-ben, éppen a többszörös öröklődés apropóján. Ugyanis pontosan ez az oka annak, hogy egy leszármazottból nem tudunk közvetlenül hivatkozni az ős egy rutinjára akkor, ha azt egyben felül is akarjuk definiálni. (Hiszen melyik ős lenne itt a super?) Ezért aztán, ha úgy akarunk egy rutint átdefiniálni, hogy felhasználjuk az ősben implementált változatot is, akkor kétszer kell származtatnunk az őstől: az egyik ágon átnevezzük az adott feature-t, hogy tudjunk rá hivatkozni, a másik ágon pedig felüldefiniáljuk, felhasználva az átnevezett régi verziót.

    Példa:

    class D inherit D rename f as old_f end; inherit D redefine f select f end feature f is do ... old_f; ... end -- f end -- class D


    Osztályhierarchia

    É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
    \ /