Oberon-2

Objektum orientált programozás

3.1 Adat absztrakció

Az absztrakt adatszerkezet egy olyan struktúrális egység, amely adatokat és eljárásokat fog össze. Az absztrakt adatszerkezet egyik fõ jellemzõje az információ elrejtés, azaz csak egy jól meghatározott ( a programozó által leírt) felület látható a külvilág számára. Az implementációs részletek e felület mögött találhatók. Az Oberon 2 lehetõséget nyújt ilyen adatszerkezetek kialakítására a modulok és a láthatósági operátorok használatával. Az alábbi példában absztrakt adattípusként egy prioritásos sort valósítunk meg:
DEFINITION PriorityQueue;
      VAR n-: INTEGER;
   PROCEDURE Insert (  x: INTEGER );
   PROCEDURE Remove ( VAR x: INTEGER );
   PROCEDURE Clear;
  END PriorityQueue.
A definíciós modul jól szemlélteti az adatszerkezetünk interfészét. Három metódus segítségével lehet megváltoztatni az adatszerkezetet. Az n a sor elemeinek számát jelenti. Ez csak olvasható, így nincs lehetoség kívülrol "bántani", és igy esetleg inkonzisztensé tenni a sort. Az implementáció további részleteit nem szükséges ismerni ahhoz, hogy használjuk a prioritásos sorunkat. Az illusztráció kedvéért:
MODULE PriorityQueue;
CONST  length = 128;
VAR
  n-: LONGINT;
  a: ARRAY length OF INTEGER;
  
PROCEDURE Clear*;
BEGIN n:=0; a[0]:= MIN(INTEGER):
END Clear;
  
PROCEDURE Insert* (x: INTEGER);
  VAR i:INTEGER;
BEGIN
  IF n < length-1 THEN
  n:= n+!;i:=n;
  WHILE x < a [ i DIV 2 ] DO
   a[i] := a[i DIV 2]; i:=i DIV 2;
  END;
  a[i]:=x;
  END
END Insert;
  
PROCEDURE Remove* (VAR x:INTEGER);
  VAR y,i,j:INTEGER; ready:BOOLEAN;
BEGIN
  IF n>0 THEN
  x:=a[1];y:=a[n];
  n:=n-1;i:=1;ready:=FALSE;
  WHILE ( i <= n DIV 2) & ~ ready DO
   j:=j+i;
   IF ( j<n ) & (a[j] <a[j+1]) THEN
   j:=j+1 END;
   IF y>a[j] THEN a[i]:=a[j];i:=j 
   ELSE ready:=TRUE END
  END;
  a[i]:=y
  END
END Remove;

  BEGIN
  Clear
END PriorityQueue;
A megoldás elõnyei és hátrányai:

3.2 Absztrakt adattípusok

Egy absztrakt adatszerkezetbõl csak egy példány létezik. Ha többet is szeretnénk használni, akkor absztrakt adattípust kell használnunk. Az Oberon 2-ben az absztrakt adattípusokat rekordként valósíthatjuk meg. Ha a rekord mezoit nem exportáljuk, akkor rejtve maradnak. Az elobbi példának megfelelõ absztrakt adattípus:
DEFINITION PriorityQueues;
  TYPE
   Queue = RECORD
    n-: INTEGER
   END;
  PROCEDURE Insert (VAR q: Queue; x: INTEGER );
    PROCEDURE Remove (VAR q: Queue; VAR x: INTEGER );
  PROCEDURE Clear(VAR q: Queue);
  END PriorityQueues;
Az absztrakt adattípus bevezetése tovább ront a hatékonyságon ui. minden egyes eljárás hívásakor paraméterként adjuk tovább a típusunkat. Erre viszont jó megoldás (és az Oberon 2- ben bevett szokás) a mutatók használata:
DEFINITION PriorityQueues1;
  TYPE
  Queue = POINTER TO QueueDesc;
  QueueDesc = RECORD
   n-: INTEGER
  END;
  PROCEDURE Insert (VAR q: Queue; x: INTEGER );
    PROCEDURE Remove (VAR q: Queue; VAR x: INTEGER );
  PROCEDURE Clear(VAR q: Queue);
END PriorityQueues1; 

3.3 Osztályok

Az absztrakt adattípusok szintaxisának legnagyobb hátránya, hogy nincs jelölve, hogy melyik eljárás hova tartozik. Ráadásul az eljárások a rekordokon kívül vannak implementálva. Az Oberon 2 új nyelvi elemei megoldást nyújtanak. A speciális, típushoz kötött eljárások használatával már megvalósul egyfajta egység, amit osztálynak is neveznek. Az osztályok több szempontból is különböznek az absztrakt adattípusoktól, de ezekrõl késobb. Az elõzõ példákat továbbfejlesztve jutunk el a prioritásos sor osztályhoz:
DEFINITION PriorityQueuesClass;
  TYPE
  Queue = RECORD
  n-: LONGINT;
  PROCEDURE  (VAR q: Queue) Insert ( x: INTEGER );
   PROCEDURE (VAR q: Queue)  Remove ( VAR x: INTEGER );
  PROCEDURE (VAR q: Queue) Clear;
  END;
END PriorityQueuesClass; 
A típushoz kötött eljárásokat ezentúl úgy használhatjuk, mintha egy rekordhoz tartozó mezõre hivatkoznánk. Ezeket az eljárásokat más néven üzeneteknek nevezik. Az adott adattípus ( osztály ) , egy eljárása valójában egy üzenet, egy kérés az osztály egy példánya felé. Az objektumot, amelynek küldjük az üzenetet fogadó objektumnak nevezzük. A fogadó objektum minden kötött eljárás paramétere. Hogy meg is különböztethessük a többi paramétertol máshogy is jelöljük:
PROCEDURE ( VAR q:Queue) Insert (x:INTEGER);

3.4 Öröklődés

Az Oberon 2 lehetoséget ad rekordok kiterjesztésére. Ezzel a módszerrel egy meglévõ rekordból létrehozhatunk egy új típust új mezõkkel eljárásokkal, és mégis kompatibilis marad a régivel:
TYPE T0 = RECORD ... END T1 = RECORD (T0) ... END
A fenti példában T1 típus T0 kiterjesztése, és T0 T1 bázis típusa. Ez bõvebben annyit jelent, hogy T1 típus T0 minden mezõjét tartalmazza és még újakat is deklarálhat. A kiterjesztés helyett sokszor az öröklõdés kifejezést is használjuk. A típus öröklõdés nemcsak rekordokra, hanem mutatókra is mûködik:
TYPE P0 = POINTER TO T0; P1 = POINTER TO T1;
Egy szemléletesebb példával:
TYPE
  Figure = POINTER  TO FigureDesc;
  FigureDesc = RECORD
  selected: BOOLEAN;
  PROCEDURE (f:Figure) Draw;
  PROCEDURE (f:Figure) Move ( x,y:INTEGER);
  PROCEDURE (f:Figure) Store ( VAR rider: OS.Rider);
END;

  Rectangle = POINTER  TO RectangleDesc;
  RectangleDesc = RECORD (FigureDesc)
  x,y,w,h: INTEGER;
  PROCEDURE (r:Rectangle) Fill (pat: Pattern);
END;

  TextBox = POINTER  TO TextBoxDesc;
  TextBoxDesc = RECORD (RectangleDesc)
  text: ARRAY 32 OF CHAR;
   END;
  VAR
   rectangle : Rectangle;
   figure: Figure;
   figureDesc: FigureDesc;
   rectangleDesc: RectangleDesc;

A rectangle változó mezõire és eljárásaira , mint rekord mezõkre lehet hivatkozni: a rectangle.selected, rectangle.Draw stb. A kiterjesztett Rectangle, Textbox típusok specializációi a Figure bázis típusnak. A kiterjesztett típusokban nemcsak új eljárásokat lehet bevezetni, de lehetõség van már meglévõek felüldefiniálására is. Az öröklõdés egyik nagy elõnye a bázis típusok és a kiterjesztett típusok kompatibilitása. Ez lényegében azt jelenti, hogy a kiterjesztett típus rendelkezik a bázis típusának minden tulajdonságával. Vagyis a fenti példa alapján: minden Rectangle objektum egyben Figure is, viszont fordítva már nem igaz. Ez azt is jelentheti, hogy egy algoritmus, ami Figure típusú objektummal dolgozik biztos, hogy mûködni fog Rectangle típusúval is, viszont nem fogja kihasználni a Rectangle minden tulajdonságát. A kompatibilitás, inkompabilitás kérdése értékadások esetében biztosan felmerül. Az alábbi értékadás biztos, hogy helyes:

figureDesc:= rectangleDesc
Ez az értékadás fordítva már fordítás idõben hibát fog eredményezni. Egy másik tipikus elofordulása a kompabilitás kérdésének az eljárások rossz paraméterezésébol ered:
PROCEDURE P (VAR figureDesc : FigureDesc);

A fenti eljárást meghívhatjuk akár RectangleDesc típusú paraméterrel is, de könnyen elõfordulhat, hogy olyan értékadás szerepel az eljárás törzsében, hogy hibát kapunk.

3.5 Statikus és dinamikus típus

Rekord ill. mutató típusú változók esetében megkülönböztetünk statikus és dinamikus típust. A változó statikus típusa az a típus, amit a változóhoz rendeltünk a deklarációja során. A dinamikus típus a változó futás idõbeni tényleges típusa, amely különbözhet a statikus típusától.

Rekord vagy mutató típusú változók dinamikus típusa futás idoben ellenõrizhetõ. Erre szolgál a típus teszt:

figure IS Rectangle

Visszatérési értéke igaz, ha a figure változó dinamikus típusa Rectangle (vagy legalábbis annak kiterjesztése). Ha a figure változó dinamikus típusa Rectangle, akkor értékül adható a rectangle változónak. Ez akkor lehetséges, ha típusõrt is meghatározunk a figure változóhoz:

figure (Rectangle)

A típusõr futásidõben ellenõrzi, hogy a figure dinamikus típusa Rectangle-e. Ha igen akkor ideiglenesen a figure statikus típusa Rectangle lesz, egyébként futás idejû hiba lép fel. A futás idejû hibát el lehet kerülni a típus teszt használatával:

IF figure IS Rectangle THEN rectangle := figure(Rectangle) END

3.6 Absztrakt osztályok

Nyelvi konstrukció nem létezik absztrakt osztályok létrehozására, de létezik megoldás ilyen osztályok kialakítására. Az absztrakt osztályok legfontosabb feladata, hogy kiemelje a leszármazottak közös tulajdonságait és egyfajta közös interfészt adjon, amely az összes leszármazottra érvényes. Az absztakt osztályok egyes metódusait, amelyeket csak a konkrét leszármazottak valósítanak meg absztrakt metódusoknak nevezzük. Ezeket a metódusokat az örököltetés során felül kell definiálnunk. Az absztrakt osztályok számos tervezési mintában is feltünnek. Példa:

TYPE 
  Stream = POINTER TO StreamDesc;
  StreamDesc = RECORD END;
  
  PROCEDURE (s: Stream) Write (ch: Char);
  BEGIN
   HALT(99);
  END Write;

  PROCEDURE (s: Stream) WriteString (a: ARRAY OF CHAR);
  VAR i:INTEGER;
  BEGIN
   i:=0;
  WHILE a[i] # 0x DO s.Write(a[i]); i:=i+1 END
  END WriteString;
  
Ennek egy leszármazottja:

TYPE 
  DiskFile = POINTER TO DiskFileDesc;
  DiskFileDesc = RECORD (StreamDesc) ... END;

  PROCEDURE (f: DiskFile) Write (ch: CHAR);  
  BEGIN
  ........
  END Write;

3.7 Generikus komponensek

Egy komponens generikus, ha különbözo típusokkal képes mûködni. Sok nyelv pl. Ada, Eiffel nyelvi szinten támogatja a generikusságot. Öröklõdés segítségével az Oberonban is tudjuk használni. Nézzünk meg erre egy példát: Egy bináris fa megvalósításához létrehozunk egy Tree osztályt. Az osztályhoz tartozó metódusok a fa elemeivel dolgoznak. A fa elemeit egy általános Node osztály valósítja meg. Késõbb ennek az osztálynak a kiterjesztésével specializálhatjuk az elem típusát.

TYPE
  Tree = RECORD
   PROCEDURE ( VAR t: Tree ) Init;
   PROCEDURE ( VAR t: Tree ) Insert ( x: Node );
   PROCEDURE ( VAR t: Tree ) Delete ( x: Node );
   PROCEDURE ( VAR t: Tree ) Search ( x: Node ): Node;
   
   Node = POINTER TO NodeDesc;
   NodeDesc = RECORD
    left,right: Node;
    PROCEDURE ( x: Node ) EqualTo ( y: Node ): BOOLEAN;
    PROCEDURE ( x: Node ) LessThan ( y: Node ): BOOLEAN;
    
    ....(* A Tree megvalósítása *)
Készítünk egy Stringet tároló Node-ot:
TYPE
  StringNode = POINTER TO StringNodeDesc;
  StringNodeDesc = RECORD ( Nodedesc )
   s: POINTER TO ARRAY OF CHAR;
  END;
  
  PROCEDURE ( x: StringNode ) EqualTo ( y: Node ): BOOLEAN;
  BEGIN RETURN x.s^ = y(StringNode).s^
  END EqualTo;
  
  PROCEDURE ( x: StringNode ) LessThan ( y: Node ): BOOLEAN;
  BEGIN RETURN x.s^ < y(StringNode).s^
  END LessThan;

3.8 Heterogén adatszerkezet

Az egyik legfontosabb alkalmazása az objektum orientált programozásnak heterogén adatszerkezetek kezelése.
Ezt szituációt a következõk jellemzik:
(1) Az objektumok különbözõ változatban fordulnak elõ.
(2) Az objektumokat használó program nem akarja megkülönböztetni a változatokat.
(3) A jövõbeli változatok számát nem ismerjük, egy újabb változat késõbb is hozzáadható.

VáltozatokMûveletek
Objektimok a szerkesztõben
(vonalak,háromszögek, körök)
Draw, Move, Click
Objektumok a képernyõn
ablakok, ikonok, menük
Draw, Move Click
Objektumok a dialógusablakban
(gombok,szövegek,görgetõsáv)
Draw, Move, Click
Objektumok egy játékban
(fogó,préda,falak)
Draw, Move, Collide
Objektumok egy szimulációban
(autók,személyek,közl. lámpák)
Activate, Delay
1. Tablázat: Példák több változatban elõforduló objektumokra

Egy grafikus szerkesztõ (editor) konvencionális implementációja

Vizsgáljuk meg azt a grafikus szerkesztõt, amely támogatja vonalak, háromszögek és körök rajzolását, kijelölését és mozgatását. A konvencionális nyelvekben, mint például a Modula-2, a többféle alakzatokat variáns rekordok segítségével implementáljuk.

TYPE Figure=POINTER TO FigureDesc; FigureDesc = RECORD next:Figure; CASE kind: FigureKind OF line: x0,y0,x1,y1: INTEGER; | rect: x,y,w,h: INTEGER; | circle: mx,my,radius: INTEGER; END; END;

Ezt a rekordtípus használva olyan lista készíthetõ, amely többféle alakzatot tartalmaz:

Azonban a variáns rekordok hazsnálata veszélyes, mert a legtöbb fordító nem generál kódot annak érdekében, hogy leellenõrizze, vajon a program a helyes változatát használja-e az objektumnak futás közben. Ráadásul bármikor, amikor egy mûvelet hivatkozik egy alakzatra, a lehetséges változatokat meg kell különböztetni a programban. Azért, hogy kirajzoljuk egy lista összes alakzatát, a következõket kéne írnunk:

figure:=firsFigure;
  WHILE figure # NIL DO (*az osszes alakzat kirajzolasa *)
   CASE fugure^.kind OF
    line: ... (* vonal rajzolasa *)
   |  rect: ... (* haromszog rajzolasa *)
   |  circle: ... (* kor rajzolasa *)
   END;
   figure:=figure^.next;
  END;

A CASE esetszétválasztások általában szét vannak szóródva az egész programszöveg területén. Ami a legrosszabb, hogy egy újabb alakzat (pl. spline-ok) bevezetése a Figure adattípus módosítását igényli, ami a kliens nodulok újrafordítását teszi szükségessé. Ráadásul minden case kifejezést hozzá kell igazítani a spline objektumokhoz is. Ez a munka fárasztó és növeli hibázás lehetõségét. Az ilyen fajta szoftver kód külalakja rendetlen és nehéz a kibõvítése.

Egy grafikus szerkesztõ objektum-orientált implementációja

Az objektum-orientált nyelvek egy jóval elegánsabb megközelítést is megengednek. Az alakzatok absztrakt objektumokként jelennek meg (fekete doboz) néhány elõfeltétellel: Legyenek listába fûzhetõk, kirajzolhatók, mozgathatók, olvashatók és tárolhatók. Ez minden, amit a szerkesztõnek tudnia kell, hogy képes legyen dolgozni az objektumokkal. Nem szükséges tudnia, hogy éppen vonallal, háromszöggel, körrel, vagy más konkrét alakzattal van dolga, és nem szükséges tudnia azt sem, hogyan kell kirajzolni, mozgatni és tárolni õket.

Absztrakt alakzatok

Ezek a meggondolások vezetnek az alábbi Figure absztrakt osztály deklarációhoz. Az OS modul leírása.

TYPE
   Figure = POINTER TO FigureDesc;
   FigureDesc = RECORD (* absztrakt *)
    next: Figure;
    selected: BOOLEAN;
    PROCEDURE (f: Figure) Draw;
    PROCEDURE (f: Figure) Move(dx,dy: BOOLEAN);
    PROCEDURE (f: Figure) HandleMouse(x,y: INTEGER;buttons: SET);
    PROCEDURE (f: Figure) Load(VAR r: OS.Rider);
    PROCEDURE (f: Figure) Store(VAR r: OS.Rider);
    ... 
  END;

Konkrét alakzatok

A konkrét alakzatok leszármazott osztályai lesznek a Figure absztrakt osztálynak. Ezek plusz adatmezõket és az absztrakt metódusok felüldefiniálását tartalmazzák.

TYPE
   Line = POINTER TO LineDesc;
   LineDesc = RECORD(FigureDesc) (* szarmaztatas *)
    x0,y0,x1,y1: INTEGER;
    PROCEDURE (ln: Line) Draw;
    PROCEDURE (ln: Line) Move(dx,dy: BOOLEAN);
    ...
  END;

  TYPE
   Rectangle = POINTER TO RectangleDesc;
   RectangleDesc = RECORD(FigureDesc) (* szarmaztatas *)
    x,y,w,h: INTEGER;
    PROCEDURE (r: Rectangle) Draw;
    PROCEDURE (r: Rectangle) Move(dx,dy: BOOLEAN);
    ...
  END;

  TYPE
   Circle = POINTER TO CircleDesc;
   CircleDesc = RECORD(FigureDesc) (* szarmaztatas *)
    mx,my,radius: INTEGER;
    PROCEDURE (c: Circle) Draw;
    PROCEDURE (c: Circle) Move(dx,dy: BOOLEAN);
    ...
  END;

Az ilyen fajta objektumok újra lehetõvé teszik heterogén adatszerkezetek konstrukcióját.

A szerkesztõben minden listabeli objektum statikus típusa Figure. A szerkesztõ csak a Figure osztály mezõit és metódusait látja, bár ténylegesen minden objektum mögött egy vonal, háromszög vagy kör rejtõzik. Az összes alakzat kirajzolásához a szerkesztõnek csak a következõket kell tennie:

figure:=firstFigure;
  WHILE figure # NIL DO
   figure.Draw;
   figure:=figure.next;
  END;

Nem kell többé esetszétválasztás

A szerkesztõnek többé nem kell megkülönböztetnie a változatokat. Minden alakzatnak egyszerûen csak a Draw üzenetet küldi, bízva abban, hogy típusától függetlenül helyesen fogja lekezelni az üzenetet. Az új Spline alakzat bevezetése nincs hatással a szerkesztõre. Ugyanúgy tudja tárolni a spline objektumokat az adatszerkezetben, mint az össze többi alakzatot, és ha egy spline kap egy Draw üzenetet, ki fog rajzolódni anélkül, hogy a szerkesztõ észrevenné a különbséget.

Erõsebb lokalizáció

Az objektumokon értelmezett mûveletek többé nincsenek szétszóródva az egész programkód területén, hanem össze vannak gyûjtve a Figure osztályban. Ez leegyszerûsíti a karbantartást. Az új spline-okat kezelõ osztály bevezetése csak ennek az egyetlen osztálynak az implementációját igényli, a program többi része változatlan marad.

Kibõvíthetõség

Kétféle kibõvítés fordul elõ ebben a példában. Az egyik a Figure osztály kibõvítése Line, Rectangle és Circle osztályokká, a másik az egész szerkesztõ kibõvítése. Eredetileg csak absztrakt alakzatokkal tudott dolgozni, most ki tud rajzolni vonalakat, háromszögekett és köröket is. Sõt bármikor kiegészíthetõ úgy, hogy képes legyen új alakzatokat kirajzolni.

Az alkzatokhoz kapcsolódó Input/Output

A grafikus szerkesztõnek idõnként el kell tárolnia az alakzatokat egy fájlban. A heterogén adatszerkezetek I/O technikája egy nemtriviális probléma. A szerkesztõ önmaga nem tudja betölteni és tárolni az alakzatokat, mert nem ismeri az adatnezõiket. Rá kell bíznia ezeket a feladatokat az alakzatokra, amelyek felülírják a Figure-tól örökölt Load és Store metódusokat. De mielõtt a szerkesztõ Load üzenetet küldhetne egy alakzatnak, elõbb létre is kell hoznia azt. Hogy honnan tudja a szerkesztõ, hogy milyen típusú objektumot kell létrehoznia, az egy furfangos kérdés. Erre késõbb még visszatérünk.

Futásidejû kibõvítés

Egy említésreméltó tulajdonsága az Oberon rendszernek, hogy a programok futás közben is kibõvíthetõek. Feltéve, hogy a szekesztõ magja két részbõl tevõdik össze: a Figures modulból, definiálva az absztrakt Figure osztályt, és az Editor modulból, ami kezeli az ablakokat, és tartalmazza az általános szerkesztési parancsokat. A Figure osztály minden egyes leszármazottja a saját moduljában van implementálva (Lines, Rectangles, Circles, stb.). Most már a szerkesztõ magja külön is elindulhat betöltve elõször a Editor, majd a Figures modulokat. Ez kompakt programot eredményez, és a betöltési idõ is elég rövid. A szerkesztõ ebben a konfigurációban egyetlen konkrét alakzattípus létezésérõl sem tud.

Amíg a szerkesztõ fut, a felhasználó úgy dönthet, hogy betölti a Lines vagy a Circles modult. Ezek a a modulok hozzá vannak linkelve a már betöltött modulokhoz, ami képessé teszi a szerkesztõt, hogy vonalakat vagy köröket rajzoljon ki. Nem mindig szükséges a szerkesztõ teljes egészét használni. Minden felhasználó betölthet saját alakzatokat anélkül, hogy problémát okozna más felhasználóknak.Lást még Futásidejû kibõvítés
Egy rensdzer igazán csak akkor bõvíthetõ, ha bárki (nem csak az író) bármikor (még futás közben is) ki tudja bõvíteni. Ez a helyzet az Oberonnál. Új modulokat, így új osztályokat lehet implementálni és betölteni, amelyek megléte ismeretlen a program számára, mégis használni tudja õket. A grafikus szerkesztõ semmit sem tud a Splines modulról amíg nincs hozzáadva. A modul hozzáadható a szerkesztõhöz anélkül, hogy módosítanánk vagy újralinkelnék azt. Az interpretált objektum-orientált rendszerekben, mint a Smalltalk, ez szintén lehetséges, de a legtöbb kompilatív renszerben nem. Az Oberon egy kivétel: Kompilatív rendszer, amely olyan fokú kibõvíthetõséget nyújt, mint a Smalltalk.

Összefoglalás

Ha egy program egy osztály többféle változatával kell hogy mûködjön, ne különböztesse meg, de kezelje õket egy absztrakt osztály kibõvítéseiként. Ilyenkor a megoldás a következõ általános komponenseket tartalmazza:
(1) Vegyük számba, hogy az összes változatban milyen közös adatok és mûveletek fordulnak elõ!
(2) Definiáljunk egy absztrakt osztályt ezekkel a tulajdonságokkal, és több származtatott osztályt a változatok részére!
(3) Dolgozzunk az absztrakt osztály változóival, anélkül hogy figylelembe vennénk,hogy valójában milyen objektumot tárol futási idõben.