4. Eiffel
Twórcą języka Eiffel jest Bertrand Meyer. Pierwsza wersja języka powstała w roku 1985, otrzymując roboczą nazwę SIMULA 85. Był to faktycznie Eiffel * unowoczeœniona wersja Simuli, uwolniona od pewnych niepotrzebnych cech i wzbogacona o nowe własnoœci. Pełny opis tej pierwszej wersji języka powstał w firmie B. Meyera, Interactive Software Engineering (ISE), i został opublikowany w roku 1986. Po wprowadzeniu szeregu zmian obowiązująca obecnie wersja języka została opublikowana w roku 1992.
Język Eiffel nie ma standardu ISO, czy też ANSI. Definicja języka jest “własnoœcią publiczną”, nadzorowaną przez społeczną organizację NICE (Non-profit International Consortium for Eiffel). W roku 1995 NICE opublikowała pierwszą wersję biblioteki ELKS-95 (Eiffel Library Kernel Standard), której klasy powinien tłumaczyć każdy kompilator.
Program Ÿródłowy w języku Eiffel składa się wyłącznie z definicji klas, w których zdefiniowano odpowiednie operacje. Tak więc, jeżeli celem programu jest wyprowadzenie na wyjœcie łańcucha znaków, zdefiniujemy klasę SIMPLE, a w niej odpowiednią operację drukowania:
class SIMPLE creation
make
feature
make is
do
io.put_string("Hello, World!;%N")
end
end --class SIMPLE
W klasie SIMPLE słowo kluczowe creation sygnalizuje nazwę operacji (make), która inicjuje jej obiekty. W instrukcji io.put_string("Hello,World!;%N")
obiekt io jest standardowym strumieniem wyjœciowym, otwieranym automatycznie po uruchomieniu programu, zaœ put_string jest operacją zdefiniowaną w klasie strumieni. Argument tej operacji jest łańcuchem znaków; dwa końcowe znaki łańcucha mają znaczenie symbolu nowego wiersza. Komentarze w języku Eiffel pisze się jako łańcuchy znaków po dwóch znakach “--”. Jeżeli klasę SIMPLE umieœcimy w pliku simple.e i wywołamy kompilator poleceniem “compile simple.e -o simple”, to otrzymamy kod wykonalny programu w pliku simple. Żądany napis na ekranie terminala otrzymamy pisząc po prostu simple.
4.1. System typów języka Eiffel
Eiffel jest językiem o typizacji statycznej. Oznacza to, że każdy program musi spełniać następujące reguły zgodnoœci:
Każda zadeklarowana w tekœcie programu nazwa, odwołująca się (w fazie wykonania) do obiektu musi być okreœlonego typu.
Każde odwołanie do atrybutu i wywołanie operacji na pewnym obiekcie musi używać dostępnego (w sensie ukrywania informacji) atrybutu lub operacji klasy tego obiektu.
Przypisanie i przekazywanie argumentu podlega regułom zgodnoœci w schemacie dziedziczenia, które wymagają, aby typ Ÿródłowy był kompatybilny z typem docelowym.
Wszystkie typy języka Eiffel są implementowane w klasach. System typów obejmuje dwa ich rodzaje: typ odnoœnikowy (reference type) i typ rozwinięty (expanded type). Wystąpienie typu odnoœnikowego jest zmienną typu implementowanego w pewnej klasie, której wartoœć jest odnoœnikiem do pewnego obiektu tej klasy, lub odnoœnikiem pustym (void). Jeżeli zadeklarujemy zmienną typu odnoœnikowego SOME_CLASS:
x: SOME_CLASS;
to po deklaracji jej wartoœć będzie void. Dopiero gdy utworzymy za pomocą operatora “!!” wystąpienie klasy SOME_CLASS (operator “!!” alokuje odpowiedni obszar pamięci dla obiektu, podobnie jak operator new w innych językach obiektowych), wywołując na zmiennej x metodę kreacji obiektu (najczęœciej o nazwie make; zadaniem tej metody jest zainicjowanie obiektu, a więc pełni ona rolę znanego w innych językach obiektowych konstruktora):
!!x.make;
wówczas wartoœcią zmiennej x będzie odnoœnik do nowo utworzonego obiektu klasy SOME_CLASS.
Wystąpienie typu rozwiniętego jest również zmienną typu implementowanego w pewnej klasie, ale wartoœcią tej zmiennej jest gotowy do użycia obiekt tej klasy. W definicji klasy implementującej typ rozwinięty słowo kluczowe class musi być poprzedzone słowem kluczowym expanded, np.
expanded class INTEGER feature ... end
Zmienna takiej klasy nie wymaga wywołania operatora “!!” przed jej użyciem. Jeżeli chcemy mieć wystąpienie klasy INTEGER, to po prostu deklarujemy:
x: INTEGER;
Powyższa deklaracja automatycznie przydziela zmiennej x pamięć dla obiektu typu INTEGER i inicjuje ten obiekt na wartoœć 0. Jeżeli teraz napiszemy instrukcję
x := 12;
będzie to oznaczać, że obiektowi x przypisano wartoœć 12.
Do typów rozwiniętych należą wszystkie typy proste: INTEGER, REAL, DOUBLE, CHARACTER i BOOLEAN. Zmienne typów prostych są w chwili deklaracji automatycznie inicjowane do niezależnych od implementacji wartoœci początkowych, jak pokazano niżej:
Typ |
Wartoœć początkowa |
INTEGER |
0 |
REAL |
0.0 |
DOUBLE |
0.0 |
CHARACTER |
Null_char |
BOOLEAN |
False |
Język Eiffel stwarza jeszcze dodatkową możliwoœć: jeżeli z jakiegoœ powodu nie chcemy mieć odnoœnika do obiektu klasy zdefiniowanej przez użytkownika (np. o nazwie SOME_CLASS), lecz sam obiekt, to możemy napisać deklarację:
x: expanded SOME_CLASS
W takim przypadku x jest zmienną typu SOME_CLASS, ale zamiast inicjować zmienną operatorem “!!”, cała pamięć potrzebna dla obiektu klasy SOME_CLASS jest przydzielana w punkcie deklaracji, a każdy atrybut klasy SOME_CLASS jest automatycznie inicjowany do wartoœci domyœlnej. Jeżeli atrybut jest typu odnoœnikowego, to zostanie automatycznie zainicjowany wartoœcią void.
Można również postąpić odwrotnie i zamiast korzystać z typów rozwiniętych dla typów prostych, używać zmiennych odnoœnikowych klas INTEGER_REF, REAL_REF, CHARACTER_REF i BOOLEAN_REF. Daje to swobodę wyboru pomiędzy odnoœnikami do obiektów, a samymi obiektami.
4.2. Klasy
Elementy składowe klas, tj. ich atrybuty (pola) oraz wykonywane na nich operacje są łącznie nazywane cechami (feature). Atrybuty mogą być typów prostych, predefiniowanych typów złożonych, lub typów odnoœnikowych. Na przykład definicja klasy BOOK:
class BOOK feature
title: STRING;
publication_date, number_of_pages: INTEGER;
author: PERSON;
end --class BOOK
zawiera atrybut title typu predefiniowanego STRING, dwa atrybuty typu prostego INTEGER oraz zmienną odnoœnikową author do wczeœniej zdefiniowanej klasy PERSON. Domyœlnie wszystkie te atrybuty są publiczne, tzn. bezpoœrednio dostępne w obiektach klasy BOOK.
Klasy wykorzystuje się bądŸ poprzez mechanizm dziedziczenia, bądŸ też poprzez zadeklarowanie po słowie kluczowym feature zmiennej odnoœnikowej typu wczeœniej zdefiniowanego, jak to uczyniono dla atrybutu author. W tym drugim przypadku mówimy, że klasa BOOK jest klientem klasy PERSON, a klasa PERSON jest dostawcą (usług) dla klasy BOOK. Relację taką nazywa się kontraktem; w ogólnoœci kontrakt pomiędzy klasą a jej klientami jest pewnym formalnym uzgodnieniem, wyrażającym prawa i obowiązki każdej ze stron.
Zadeklarujmy zmienną odnoœnikową typu BOOK, np. bref:BOOK; jej (domyœlną) wartoœcią początkową będzie void (odnoœnik pusty), a następnie utwórzmy obiekt klasy BOOK. Ponieważ klasa BOOK nie zawiera frazy `creation' i nie ma zdefiniowanej funkcji inicjującej jej pola do wartoœci innych niż domyœlne, obiekt tej klasy tworzy się po prostu przez wywołanie operatora “!!” z argumentem bref, tj. przez wykonanie instrukcji !!bref. Wykonanie tej instrukcji obejmuje następujące trzy etapy:
Utworzenie nowego wystąpienia klasy BOOK, nazwijmy go ob. Wystąpienie ob będzie zbiorem pól, po jednym polu dla każdego atrybutu.
Zainicjowanie każdego pola BOOK zgodnie ze standardowymi wartoœciami domyœlnymi.
Dołączenie wartoœci bref (odnoœnika) do obiektu ob.
Klasa może zawierać więcej niż jedną sekcję, wprowadzaną słowem kluczowym feature. Eiffel oferuje także prosty, ale jednoczeœnie bardzo giętki mechanizm sterowania dostępem do atrybutów i metod, nazywanych tu cechami. Cechy wymieniane zaraz po słowie feature są dostępne dla wszystkich klientów klasy. Dostęp można ograniczyć, pisząc po słowie feature nazwę klasy, lub przedzielone przecinkami nazwy kilku klas w nawiasach klamrowych; jeżeli chcemy, aby różne cechy miały różne ograniczenia dostępu, definiujemy dwie lub więcej sekcji feature. Np. w klasie
class P feature
f ...
g ...
feature{A,B}
h ...
...
end
cechy f oraz g będą dostępne dla wszystkich klientów, zaœ cecha h jedynie dla klas A i B oraz dla ich klas potomnych. Oznacza to, że dla x:P odwołanie x.h będzie błędem syntaktycznym, chyba że wystąpi w definicji jednej z klas A, B lub jednej z ich klas potomnych.
Jeżeli chcemy ukryć pewną cechę przed wszystkimi klientami, wtedy po słowie feature piszemy w nawiasie klamrowym NONE:
class P feature
exported
feature{NONE}
secret
end
Cecha o nazwie exported jest publicznie dostępna, zaœ cecha o nazwie secret jest prywatną cechą klasy P. NONE jest klasą ze standardowej biblioteki ELKS; nie ma ona ani wystąpień, ani klas potomnych. Przy takiej definicji, odwołanie kwalifikowane x.secret będzie błędem syntaktycznym; do cechy tej można się odwoływać jedynie w definicji metody z klasy P lub jej klasy potomnej.
Szczególnym rodzajem atrybutów są stałe. W programach czysto obiektowych również stałe powinny być obiektami, współdzielonymi przez wiele obiektów tej samej klasy lub obiekty różnych klas. Eiffel rozróżnia stałe typów prostych, stałe typu klasy oraz stałe wprowadzane przez funkcję once. W przypadku typów prostych stałe są atrybutami definiowanymi w klasie; wprowadza się je słowem kluczowym is po deklaracji typu, jak pokazano niżej:
Max: INTEGER is 100;
Pi: REAL is 3.14;
Better_pi: DOUBLE is 3.1415926;
Ok: BOOLEAN is True;
Menu_option: CHARACTER is *M*
Tak zdefiniowane stałe symboliczne Max, Pi, Better_pi, Ok i Menu_option mogą być albo eksportowane (używalne przez obiekty innych klas), albo prywatne w klasie. W przeciwieństwie do innych atrybutów stałe symboliczne nie zajmują dodatkowego obszaru pamięci w obiektach klasy, w której zostały zdefiniowane. Jeżeli w pewnych obliczeniach musimy używać wielu specyficznych stałych bez wiązania ich z konkretnymi obiektami (np. w edytorach tekstu), wówczas grupujemy je w klasie, która nie będzie miała wystąpień, lecz będzie klasą rodzicielską dla klas korzystających z tych stałych. Odpowiednie deklaracje mogą mieć postać:
class EDITOR_CONSTANTS feature
Insert: CHARACTER is `i'
Delete: CHARACTER is `d'; --etc
...
end
class SOME_CLASS_FOR_THE_EDITOR inherit
EDITOR_CONSTANTS
...Inne klasy rodzicielskie
feature
...
end
Zauważmy, że klasa SOME_CLASS_FOR_THE_EDITOR może wymagać innych klas rodzicielskich, a zatem pokazany wyżej schemat wymaga dziedziczenia mnogiego.
Implementacja operacji w klasie może być procedurą lub funkcją; procedura wykonuje pewne działanie, które może zmienić stan obiektu, zaœ funkcja zwykle oblicza pewną wartoœć w oparciu o stan obiektu. Procedury i funkcje nie mogą być zagnieżdżane. Postać definicji procedur i funkcji można zademonstrować na przykładzie klasy POINT:
class POINT
creation make
feature
x, y: REAL;
make(vx, vy: REAL) is
do
x := vx; y := vy;
end;--make
scale(factor:REAL) is
do
x := factor*x;
y := factor*y;
end; --scale
translate(a,b:REAL) is
do
x := x+a; y := y+b;
end; --translate
distance(other:POINT): REAL is
do
Result := sqrt((x-other.x)^2+(y-other.y)^2)
end; --distance
distance_to_origin: REAL is
local
origin: POINT
do
!!origin.make(0,0);
Result := distance(origin)
end --distance_to_origin
end - class POINT
Definicja klasy POINT zawiera definicje dwóch procedur (scale i translate) i dwóch funkcji (distance oraz distance_to_origin). Każda procedura lub funkcja jest wprowadzana słowem kluczowym is, zaœ ich instrukcje, zawarte pomiędzy słowami kluczowymi do i end, przedzielone są œrednikami, pełniącymi rolę separatorów. Funkcja distance_to_origin zawiera ponadto zmienną lokalną origin. Zauważmy, że obydwie funkcje zawierają instrukcję przypisania wartoœci obliczonego wyrażenia do (predefiniowanej) zmiennej Result, przekazującej wynik funkcji. Result jest specyficznym dla języka Eiffel sposobem zwracania wartoœci przez funkcję; przy wywołaniu przybiera automatycznie standardową wartoœć początkową, przewidzianą dla typu zwracanego, zgodnie z regułami języka. Ponieważ w ciele funkcji distance_to_origin jest wywoływana funkcja distance z argumentem typu POINT, tworzony jest obiekt klasy POINT instrukcją !!origin.make(0,0). Jeżeli funkcja distance_to_origin będzie wywoływana wielokrotnie, wówczas pokazana wyżej jej implementacja okaże się nieefektywna, ponieważ za każdym razem zmienna (referencja) origin będzie inicjowana na wartoœć void i za każdym razem będzie tworzony nowy obiekt klasy POINT, reprezentujący punkt (0,0).
4.3. Procedury tworzenia obiektów
Obiekty są wielkoœciami fazy wykonania. Jeżeli w klasie nie zdefiniowano żadnej procedury inicjującej atrybuty do wartoœci innych niż domyœlne, obiekty są tworzone w miarę potrzeby za pomocą operatora “!!” przykładanego do zmiennej odnoœnikowej. Gdyby np. w prezentowanej wyżej klasie POINT nie było procedury make, konstrukcja obiektu tej klasy z domyœlnymi wartoœciami atrybutów (x,y) mogłaby mieć postać deklaracji:
p1: POINT; !!p1;
Taki sposób kreacji jest wystarczający dla obiektów, których atrybuty są całkowicie od siebie niezależne. Jednak w większoœci przypadków atrybuty obiektów muszą spełniać pewne warunki zgodnoœci. Typowym przykładem może być obiekt reprezentujący osobę, w którym mamy atrybut wiek i inny atrybut rok_urodzenia. Wartoœci tych dwóch atrybutów nie mogą być ustawione na dowolne wartoœci: suma atrybutu rok_urodzenia i atrybutu wiek musi dać albo rok bieżący, albo rok poprzedni.
Dla zainicjowania atrybutów do wartoœci innych niż domyœlne w ciele klasy definiuje się procedury kreacji wprowadzane słowem kluczowym creation. Procedury te mają zwyczajowo nazwę make lub też make_ jest przedrostkiem pełnej nazwy procedury. Przykładowa deklaracja może mieć postać:
class A creation
make_1, make_2, ...
feature
...deklaracje cech, w tym procedur make_1, make_2, ...
end
Dla zmiennej odnoœnikowej a:A instrukcja tworzenia obiektu klasy A nie będzie już !!a, lecz !!a.make_1(...)lub !!a.make_2(...) gdzie (...) jest wykazem argumentów aktualnych dla make_1 albo make_2 (wywołania !!a i !!a.make(...) wzajemnie się wykluczają). Efektem wykonania takiej instrukcji jest utworzenie obiektu z domyœlnymi wartoœciami atrybutów (jak w przypadku braku procedur make), a następnie wywołanie na tym obiekcie make z podanymi argumentami. Na przykład w klasie POINT moglibyœmy zadeklarować dwie procedury make:
class POINT1 creation
make_cartesian, make_polar
feature
x,y,:REAL
feature{NONE}
make_cartesian(a,b:REAL) is
do x:=a; y:=b end;
make_polar(r,t:REAL) is
do x:=r*cos(t); y:=r*sin(t) end
end--POINT
Przy takiej postaci deklaracji obydwie procedury make są prywatne w klasie POINT. Nie można ich wywoływać na istniejącym obiekcie, np. my_point.make_cartesian(0,1), ale można przy ich pomocy tworzyć obiekty tej klasy, np. instrukcją !!my_point.make_polar(1,Pi/2). Można także, w razie potrzeby, dać możliwoœć kreacji obiektów danej klasy innym klasom wymienionym w nawiasach klamrowych po słowie kluczowym creation, np.
class C creation{A,B, ...} m1, m2 ... end
W pewnych przypadkach może być potrzebne zarówno inicjowanie obiektu wartoœciami domyœlnymi, jak i zadanymi w procedurach kreacji. Wtedy jedną z procedur deklarowanych po creation powinna być bezargumentowa procedura nothing. Ciało tej procedury jest puste: nothing is do end, a sama procedura jest dziedziczona z klasy ANY. Innym przypadkiem szczególnym może być definicja klasy, która nie daje prawa tworzenia swoich wystąpień żadnemu klientowi:
class C creation
--Pusty wykaz procedur kreacji
feature
...
end
Dla pojedynczej klasy taka definicja nie ma większego sensu. Jeżeli jednak klasa C jest pomyœlana jako klasa rodzicielska dla kolejnych klas potomnych, wtedy jest to konstrukcja wysoce przydatna.
4.4. Obiekty współdzielone
W języku Eiffel nie ma zmiennych globalnych, których wartoœci mogłyby zostać zmienione w procedurach. Nie stosuje się w nim również technik współdzielenia obiektów takich jak opóŸnione inicjowanie, bądŸ wykorzystanie wzorca projektowego Singleton. Zamiast tego używa się predefiniowanej funkcji once, która pozwala współdzielić różnym obiektom wspólne zasoby (jest to mechanizm, podobny do deklaracji zmiennych klasy w innych językach obiektowych). Uzyskuje się dzięki temu następujące korzyœci:
Współdzielony obiekt jest tworzony automatycznie przy pierwszym odwołaniu do niego, eliminując częste w innych językach błędy “niezainicjowanej zmiennej”.
Współdzielony obiekt wykorzystuje swoją własną procedurę tworzenia. Wskutek tego mamy pewnoœć, że pierwszy użytkownik obiektu poprawnie go zainicjuje.
Współdzielony obiekt musi być zadeklarowany w ciele klasy, której wszystkie wystąpienia będą mieć do niego dostęp. Klasa deklarująca może uczynić współdzielony obiekt widzialnym dla ograniczonego kręgu jej klientów w wykazie cech eksportowanych; np. gdy klasa A eksportuje cechę f jedynie do klasy B, jak w deklaracji: class A feature{B} f ... end, wówczas f jest dostępna w klasie B i jej klasach potomnych.
Funkcja once jest podobna do zwykłej funkcji, za wyjątkiem tego, że jej ciało zaczyna się od słowa kluczowego once zamiast do. Ciało tej funkcji jest wykonywane tylko przy pierwszym wywołaniu; następne wywołania nie powodują wykonania ciała funkcji once, lecz jedynie zwrócenie tej samej wartoœci, co przy pierwszym wywołaniu. Tak np. poprawę efektywnoœci funkcji distance_to-origin uzyskamy przez jej redefinicję
distance_to_origin: REAL is
local
origin: POINT
once
!!origin.make(0,0);
Result := distance(origin)
end --distance_to_origin
Funkcję once można wykorzystać do definiowania stałych strukturalnych. Np. w klasie COMPLEX możemy zdefiniować stałą im, której częœć rzeczywista jest równa 0, a częœć urojona 1:
class COMPLEX creation
make
feature
x,y: REAL;
make(a,b:REAL) is
do x := a; y := b end;
im: COMPLEX is
once !!Result.make(0,1) end
end
4.5. Dziedziczenie i polimorfizm
Mechanizm dziedziczenia obejmuje dziedziczenie pojedyncze oraz dziedziczenie mnogie. Ilustracją może być klasa rodzicielska FIGURE
class FIGURE creation make
feature ...
end --class Figure
od której klasa potomna POLYGON dziedziczy jej cechy i specjalizuje pewne cechy własne, co jest sygnalizowane słowem kluczowym inherit:
class POLYGON
inherit FIGURE
feature
make is do ... end;
vertices:POINT;
perimeter is do ... end
...
end --class POLYGON
Kolejną specjalizacją może być klasa RECTANGLE, dziedzicząca od klasy POLYGON:
class RECTANGLE
inherit
POLYGON redefine perimeter end;
feature
side1,side2,diagonal: REAL;
make(center:POINT;s1,s2,angle:REAL)
do ... end;--make
perimeter:REAL is
do Result := 2*(side1+side2) end--perimeter
end
Zwróćmy uwagę na słowo kluczowe redefine, które sygnalizuje zmianę definicji funkcji perimeter obliczającej obwód figury. Ponadto, ponieważ procedury kreacji nie są dziedziczone, w definicji klasy RECTANGLE musieliœmy również umieœcić nową definicję procedury make.
Wbudowane w język reguły zgodnoœci typów pozwalają wiązać referencję do klasy rodzicielskiej z obiektem klasy potomnej. Dla pokazanych wyżej definicji możemy np. napisać:
p: POLYGON;
!!p.make;
r: RECTANGLE;
!!r.make(...);
p := r;
Przypisanie p := r oznacza jedynie przypisanie odnoœników; ponieważ zmienna r została skojarzona z obiektem klasy RECTANGLE, po przypisaniu zmienna p będzie skojarzona z tym samym obiektem. Jest to najprostszy przykład polimorfizmu: wszędzie tam, gdzie występuje odwołanie do obiektu klasy rodzicielskiej, możemy je zastąpić odwołaniem do obiektu jej klasy potomnej. Dzięki temu można np. utworzyć tablicę, której elementami będą odnoœniki do obiektów różnych klas, dziedziczących od klasy POLYGON. Zauważmy jednak, że typ statyczny zmiennej odnoœnikowej, tzn. jej typ deklarowany w klasie (np. p: POLYGON) nie ulegnie zmianie również w fazie wykonania; zmianie ulegnie natomiast jej typ dynamiczny (z POLYGON na RECTANGLE). Nie dotyczy to zadeklarowanego typu obiektu: utworzony instrukcją !!r.make(...) obiekt klasy RECTANGLE będzie miał typ RECTANGLE także w fazie wykonania.
Polimorfizm zmiennych odnoœnikowych ma bardzo ważną implikację. Gdyby ograniczyć się do instrukcji
p: POLYGON;
!!p.make;
p.perimeter;
to zostanie wywołana operacja perimeter zdefiniowana w klasie POLYGON. Jeżeli zaœ do p przypiszemy odnoœnik r do obiektu klasy RECTANGLE, to będzie wywołana operacja perimeter, której metoda została zredefiniowana w klasie RECTANGLE. Stanie się tak dzięki wbudowanej w język regule wiązania dynamicznego, która stanowi, że będzie wywołany ten wariant operacji, który został zdefiniowany dla typu dynamicznego zmiennej odnoœnikowej.
Dziedziczenie może być traktowane zarówno jako specjalizacja, lub jako rozszerzenie klasy rodzicielskiej. W pierwszym przypadku, jeżeli B jest potomkiem A (B jest bardziej specjalizowanym pojęciem niż A), obiekty, które mogą być kojarzone w fazie wykonania z wielkoœcią typu B (wystąpienia klasy B i jej klas potomnych) tworzą podzbiór zbioru obiektów, które mogą być kojarzone z wielkoœcią typu A (wystąpienia klasy A i jej klas potomnych). Jeżeli natomiast klasę potraktujemy jako dostawcę usług, to stwierdzimy, że klasa B oferuje usługi odziedziczone z klasy A plus swoje własne. Z tego punktu widzenia zbiór cech klasy A jest podzbiorem cech klasy potomnej B.
Dziedziczenie mnogie jest również sygnalizowane słowem kluczowym inherit, po którym wymienia się nazwy klas rodzicielskich, oddzielone œrednikami:
class C creation make
inherit
A;
B
feature ... end
Przy dziedziczeniu mnogim może wystąpić kolizja nazw; dla pokazanej wyżej definicji w klasach A i B mogą wystąpić elementy (atrybuty lub operacje) o tej samej nazwie, np. el; w takim przypadku pokazana definicja będzie błędna. Kolizję nazw można usunąć na kilka sposobów, w zależnoœci od tego, co chcemy przy tym osiągnąć. Jeżeli chcemy w klasie C mieć tylko jeden egzemplarz cechy el, dziedziczony z klasy A, zastosujemy składnię:
class C creation make
inherit
A select el;
B
feature ... end
Gdyby cecha el miała pochodzić z klasy B, wówczas zamiast frazy
A select el
napiszemy
B select el.
Jeżeli natomiast chcemy w klasie C mieć dostęp do obu cech (tj. el z klasy A i el z klasy B), to użyjemy frazy rename, zmieniając nazwę jednej z cech w klasie potomnej:
class C creation make
inherit
A rename el as A_el;
B
end;
feature ... end
Mechanizm zmiany nazwy, oprócz likwidacji kolizji, wykorzystuje się także w głębokich drzewach dziedziczenia, dzięki czemu użytkownik klasy potomnej niskiego poziomu może korzystać z nowego zbioru nazw.
Cechy, do których dostęp ograniczono w klasie rodzicielskiej, mają te same ograniczenia dostępu w klasie potomnej. Możliwa jest jednak zmiana praw dostępu w klasie potomnej za pomocą frazy export. Niech np. klasą rodzicielską będzie
class VEHICLE
feature{DRIVER}
id: INTEGER
end
Jeżeli definicja klasy potomnej ma postać
class LAND_VEHICLE
inherit VEHICLE
end
to dostęp do atrybutu id ma nadal tylko klasa DRIVER. Prawa dostępu możemy zmienić w definicji:
class LAND_VEHICLE
inherit VEHICLE
export{MECHANIC, FBI} id end;
end
Jeżeli zamiast nazwy atrybutu (np. id) napiszemy słowo kluczowe all, wówczas zostaną wyeksportowane do wymienionych w nawiasie klamrowym klas wszystkie atrybuty z danej sekcji feature.
Cechę dziedziczoną można także zignorować w klasie potomnej. Na przykład w definicji
class NEW_CLASS
inherit OLD_CLASS
undefine useless_routine end;
end
Ponieważ zawężanie liczby usług w klasie potomnej mogłoby być w pewnych sytuacjach niebezpieczne, lub niewygodne, wprowadzono do języka mechanizm “zamrożenia”: metoda, której nazwę poprzedza słowo kluczowe frozen, nie może podlegać w klasie potomnej ani zmianie nazwy (rename), ani redefinicji (redefine), ani zignorowaniu (undefine).
4.6. Klasy abstrakcyjne i parametryzowane
Przy omawianiu dziedziczenia pokazano możliwoœć redefinicji metod w klasach potomnych. W pewnych przypadkach może być konieczne wymuszenie takich redefinicji, w szczególnoœci gdy klasa rodzicielska ma zawierać metody, których implementacje odkłada się do klas potomnych. Metoda z odłożoną “na póŸniej” implementacją jest metodą abstrakcyjną, a klasa zawierająca co najmniej jedną taką metodę jest klasą abstrakcyjną, a więc * ze względu na niepełną implementację * nie może mieć wystąpień. Przykładem klasy abstrakcyjnej może być użyta w wierzchołku drzewa dziedziczenia klasa FIGURE, jeżeli zadeklarowano w niej procedurę abstrakcyjną translate:
translate(a,b:REAL) is deferred end --translate
Jak widać, w metodzie abstrakcyjnej częœć instrukcyjna jej ciała (do instrukcje) zastępuje się słowem kluczowym deferred. Jeżeli w klasie zadeklarowano metodę abstrakcyjną, wówczas definicja tej klasy musi się zaczynać od frazy deferred class, np.
deferred class FIGURE
feature ...
end --class Figure
Klasa dziedzicząca od klas abstrakcyjnych będzie tzw. klasą efektywną, tj. klasą, która może mieć wystąpienia, jeżeli podamy w niej pełne definicje wszystkich metod abstrakcyjnych; w przeciwnym przypadku będzie ona także klasą abstrakcyjną, nawet gdy nie wprowadza ona swoich własnych metod abstrakcyjnych. Zauważmy także, iż definicje metod abstrakcyjnych w klasie potomnej nie są redefinicjami, a więc nie będą poprzedzane słowem kluczowym redefine.
Klasy parametryzowane wzorowane są na analogicznym pojęciu z języka Ada, tj. klasy z parametrami formalnymi reprezentującymi typy. Parametryzacja uzupełnia dziedziczenie, w tym także dziedziczenie od klas abstrakcyjnych. Składnię deklaracji klas parametryzowanych ilustruje definicja klasy ARRAY, reprezentującej tablice jednowymiarowe:
class ARRAY[T] creation make
feature
lower,upper,size:INTEGER;
make(minb,maxb:INTEGER) is do ... end;
entry(i:INTEGER): T is do ... end;
enter(i:INTEGER; value: T) is do ... end
end - class ARRAY
Funkcja entry zwraca wartoœć elementu tablicy; procedura enter zmienia wartoœć elementu; make tworzy tablicę dynamiczną.
Sposób korzystania z tak zdefiniowanej klasy pokazuje poniższa sekwencja instrukcji, w której parametr formalny T zastąpiono parametrem aktualnym * nazwą klasy POINT:
pa:ARRAY[POINT]; p1:POINT; i,j:INTEGER
...
!!pa.make(-32,101) -- Utwórz tablicę o zadanej minimalnej i maksymalnej -- wartoœci indeksu
pa.enter(i,p1) -- Przypisz p1 do elementu o indeksie i
p1 := pa.entry(j) -- Przypisz do p1 wartoœć elementu o indeksie j.
Klasa może być parametryzowana więcej niż jednym parametrem, np.
class C[T1,T2, ..., Tn] ...
gdzie parametry mogą reprezentować zarówno typy proste, jak i klasy. Również klasa abstrakcyjna może być parametryzowana, przy czym w jej klasach potomnych po słowie kluczowym inherit może być wymieniona klasa abstrakcyjna zarówno z parametrem formalnym, jak i aktualnym.
4.7. Asercje
Asercje są to wyrażenia logiczne o postaci “A jest B”, których wartoœcią jest prawda lub fałsz. Asercje można traktować jako pomost pomiędzy pojęciem klasy a teorią abstrakcyjnych typów danych. Pozwalają one okreœlić pewne formalne własnoœci, które muszą spełniać operacje na obiektach. Wyróżnia się asercje wejœciowe (preconditions) wprowadzane słowem kluczowym require, asercje wyjœciowe (postconditions) wprowadzane słowem kluczowym ensure i niezmienniki klas (class invariants) wprowadzane słowem kluczowym invariant. Asercja wejœciowa sprawdza warunki, jakie muszą być spełnione przy wywołaniu metody; asercja wyjœciowa opisuje własnoœci gwarantowane przy zakończeniu działania metody; niezmiennik klasy jest asercją, która musi być spełniona przy tworzeniu każdego wystąpienia klasy i zachowana przez każdą eksportowaną metodę klasy. Są to bardzo użyteczne narzędzia, wspomagające projektowanie poprawnego formalnie oprogramowania, automatyczną dokumentację i uruchamianie.
Niżej pokazano przykładową definicję klasy ACCOUNT z wprowadzonymi asercjami:
class ACCOUNT creation
make
feature
balance: INTEGER;
minimum_balance: INTEGER is 100;
owner: STRING;
open(who:STRING) is do owner := who end;
add(sum:INTEGER) is do balance := balance+sum end;
deposit(sum:INTEGER) is
require sum >= 0
do add(sum)
ensure balance = old balance+sum end;
withdraw(sum:INTEGER) is
require 0 <= sum;sum <= balance-minimum_balance
do add(-sum)
ensure balance = old balance-sum
end;
make(initial:INTEGER) is
require initial >= minimum_balance
do balance := initial end;
invariant balance >= minimum_balance
end --class ACCOUNT
Występująca w asercjach wyjœciowych notacja old wyrażenie (gdzie wyrażenie jest atrybutem lub parametrem aktualnym) oznacza wartoœć, jaką wyrażenie miało przy wejœciu do metody; wyrażenie w asercji wyjœciowej nie poprzedzone przez old oznacza wartoœć wyrażenia przy wyjœciu z metody. Klasa ACCOUNT zawiera procedurę make z parametrem initial, która zapewnia prawdziwoœć niezmiennika klasy. Procedura make zapewnia spełnienie asercji wejœciowej, jeżeli podejmiemy próbę utworzenia obiektu z wartoœcią parametru initial większą lub równą 100.
Tak wprowadzone asercje, oprócz korzyœci wymienionych na wstępie, stwarzają możliwoœć wypełnienia kontraktu, wiążącego metodę z klientem, który ją wywołuje. Asercja wejœciowa stosuje się do wszystkich wywołań metody zarówno z wnętrza klasy, jak i klienta zewnętrznego; poprawnie zbudowany system nigdy nie wykona wywołania w stanie, który nie spełnia warunków wejœciowych; asercja wyjœciowa gwarantuje, że metoda wprowadzi system w stan zapewniający pewne własnoœci, przy założeniu, że metoda została wywołana ze spełnioną asercją wejœciową. Obrazowo kontrakt ten można przedstawić tak, jak gdyby klasa mówiła do swoich klientów wywołujących metodę m: “jeżeli obiecacie mi, że będziecie wywoływać m ze spełnioną asercją wejœciową, wtedy ja w zamian obiecuję dostarczyć taki stan końcowy, w którym będzie spełniona asercja wyjœciowa”.
4.8. Globalna struktura dziedziczenia
W języku Eiffel wszystkie klasy dziedziczą niejawnie od klasy bibliotecznej ANY; klasa ANY dziedziczy z kolei od klasy GENERAL (w SmallEiffel pomiędzy ANY a GENERAL jest jeszcze klasa poœrednicząca PLATFORM), w której zdefiniowano pewną liczbę cech dziedziczonych przez klasy potomne. Cechami tymi są powszechnie stosowanie operacje: kopiowania obiektów (copy), płytkiego (clone) i głębokiego (deep_clone) klonowania obiektów, porównania obiektów (equal i deep_equal) oraz podstawowe operacje wprowadzania/wyprowadzania (print i print_line). Konsekwencją niejawnego dziedziczenia od klasy ANY jest równoważnoœć zapisów prawa dostępu do pewnych cech: zapis feature ... wykaz cech jest równoważny zapisowi feature{ANY} ... wykaz cech. Obydwa zapisy oznaczają możliwoœć wyeksportowania cech z wykazu do wszystkich klas użytkownika. Podobnie można stwierdzić, że deklaracja reeksportu cechy klasy rodzicielskiej do wszystkich klas użytkownika może być zapisana w postaci feature{ANY} cecha lub po prostu feature cecha.
Specyficzną * nie występującą w żadnym języku obiektowym * własnoœcią hierarchii klas jest istnienie klasy NONE; klasa ta nie ma żadnych wystąpień i dziedziczy od każdej klasy, która nie ma żadnych klas potomnych. Tak ustawione w hierarchii dziedziczenia klasy ANY i NONE czynią z niej strukturę kraty, jak pokazano na rysunku 4-1.
Rys. 4-1. Globalna struktura dziedziczenia w języku Eiffel
Osobliwoœcią pokazanej na rysunku struktury jest fakt, że ilekroć użytkownik definiuje klasy z dziedziczeniem mnogim, zawsze wynikowa hierarchia klas będzie miała postać skierowanego grafu acyklicznego. Jedną z konsekwencji jest doœć skomplikowany sposób wywołania metody klasy rodzicielskiej w zredefiniowanej metodzie klasy potomnej. Można to uczynić np. przez powtórzone dziedziczenie: jeżeli mamy wywołać w klasie potomnej metodę `foo', to musimy wziąć dwie wersje `foo' i zredefiniować jedną z nich:
class PARENT feature foo is do ... end
end--PARENT
class CHILD
inherit
PARENT
rename foo as parent_foo end;
PARENT--powtórzone dziedziczenie
redefine foo end;
select foo --w przypadku wiązania dynamicznego
end
feature
foo is do parent_foo ... end
end--CHILD
Innym sposobem jest zamrożenie jednej z wersji metody klasy rodzicielskiej:
class PARENT
feature foo, frozen parent_foo is do ... end
end--PARENT
class CHILD
inherit
PARENT redefine foo end;
feature
foo is do parent_foo ... end
end--CHILD
W powyższej deklaracji cechy foo i parent_foo są synonimami; obydwie cechy, których nazwy przedzielono przecinkiem, współdzielą tę samą definicję, ale tylko jedna z nich (foo) może być redefiniowana.
Dla uniknięcia tej komplikacji wprowadzono zmianę w definicji języka, polegającą na wprowadzeniu nowego słowa kluczowego Precursor, po którym może być wołana, bez uprzedniej zmiany nazwy, wersja metody z klasy rodzicielskiej.
Wprowadzenie w pewnym sensie fikcyjnej klasy NONE daje szereg korzyœci. Jedną z nich jest to, że NONE jest typem dla wartoœci `void', którą można przypisać zmiennej dowolnego typu odnoœnikowego. Inną korzyœcią jest możliwoœć ukrycia pewnej cechy przed wszystkimi klientami, co zapisuje się w postaci feature{NONE}.
4.9. Mechanizm wyjątków
Wyjątek jest zdarzeniem fazy wykonania, które może spowodować niepowodzenie w wykonaniu metody, nazywanej w języku Eiffel “routine”. Język Eiffel wprowadza frazę rescue, będącą de facto sekwencją instrukcji oraz instrukcję retry dla zgłaszania i obsługi wyjątków. Ponadto, w przypadkach gdy chcemy uzyskać bliższą informację o ostatnim zgłoszonym wyjątku, możemy wykorzystać klasę biblioteczną EXCEPTIONS.
Mechanizm wyjątków włącza się wtedy, gdy metoda wykryje, iż nie jest zdolna wypełnić kontraktu ze swoim klientem. Może to być spowodowane trzema czynnikami:
klient nie wypełnił swojej częœci kontraktu (została naruszona asercja wejœciowa),
nie zostało wykonane wywołanie metody z ciała metody wykonywanej,
została naruszona asercja wyjœciowa.
Powyższe względy zdecydowały, że fraza rescue, jeœli występuje, pojawia się tuż przed końcowym end po obu wymienionych asercjach, jak w przykładowej metodzie:
metoda is
require ...
local ...
do
ciało
ensure ...
rescue
fraza rescue
end
Przy takiej deklaracji pojawienie się wyjątku podczas wykonywania ciała metody powoduje jego zatrzymanie i przejœcie do wykonania frazy rescue. Celem frazy rescue jest przywrócenie obiektu, na którym została wywołana dana metoda, do stanu stabilnego (spełniającego niezmiennik klasy). Jeżeli fraza rescue jest wykonywana do końca, metoda zostaje zaniechana, powodując zgłoszenie wyjątku u wywołującego ją klienta. Fraza rescue może również się zakończyć wykonaniem instrukcji retry, która będzie próbowała powtórnie wykonać metodę; w tym przypadku zakłada się, że fraza rescue naprawiła przyczynę wystąpienia wyjątku i zapewniła spełnienie asercji wejœciowej. Jeżeli ta próba się powiedzie, wyjątek nie będzie powtórnie zgłoszony; ilustracją może być następujący przykład:
attempt_to_write(x:INTEGER) is
--Próba zapisu (co najwyżej trzykrotna) na dyskietke
require ...
local attempts, failed:INTEGER
do
actual_write(x)
--actual_write jest fizyczną operacją zapisu
--która może zgłosić wyjątek
ensure ...
rescue
attempts := attempts+1;
if failed <= 3 then
io.put_string("Disk write failed.Check if unit is on-line.;%N");
io.put_string("Type any character when ready to try again.;%N");
readchar;
retry
end --if
end -attempt_to_write
Powyższa procedura próbuje zapisać na dyskietkę liczbę typu INTEGER. Jeżeli operacja się nie powiedzie (np. z powodu braku sygnału gotowoœci), zostaje wyprowadzony komunikat o błędzie i próba będzie trzykrotnie ponawiana. Zakłada się, że fizyczna operacja zapisu może zgłosić wyjątek.
4.10. Zbieranie nieużytków
Kolektor nieużytków jest częœcią œrodowiska języka Eiffel. Koncepcyjnie jest to kolektor równoległy, wykonywany jako współprogram (coroutine). Współprogram można uważać za pewną procedurę wykonywaną w oddzielnym wątku; zachowuje ona wartoœci swoich lokalnych danych od jednej aktywacji do następnej, a każde wznowienie jej wykonania zaczyna się w punkcie, w którym zatrzymało się poprzednie. Wykonanie programu (aplikacji) w języku Eiffel można traktować jako swoisty “wyœcig” pomiędzy dwoma współprogramami: aplikacją, która kreuje obiekty i czyni niektóre z nich “martwymi” oraz kolektorem nieużytków, który podąża za aplikacją, zbiera wszystkie znalezione martwe obiekty i oddaje aplikacji zajmowany przez nie obszar pamięci.
Kolektor nieużytków działa w zasadzie według algorytmu “oznaczanie-wymiatanie” (mark-sweep). W fazie oznaczania kolektor przebiega aktywną częœć struktury obiektowej, zaczynając od obiektów klasy najwyższej w hierarchii i znakuje jako “żywe” wszystkie napotkane obiekty. W fazie wymiatania przebiega liniowo całą strukturę, wstawiając wszystkie obiekty nieoznakowane do listy wolnych obszarów pamięci i usuwając oznakowanie wszystkich obiektów.
Kolektor jest uaktywniany, gdy system wykonawczy wykryje nadmierną zajętoœć pamięci. Ponieważ kolektor jest zorganizowany jako pętla nieskończona, która może być przerwana w dowolnym punkcie, system wykonawczy steruje czasem aktywacji każdego włączenia kolektora. Jest to adaptacyjny algorytm sterowania, który stara się utrzymać stan równowagi w wielkoœci zajętego obszaru pamięci. Idealna równowaga zapewnia stałą wielkoœć tego obszaru: aplikacja po każdej kreacji nowego obiektu zgłasza jeden obiekt jako martwy, a kolektor niezwłocznie odzyskuje ten obszar.
Gdy równowaga zostanie naruszona i wielkoœć zajętego obszaru pamięci wzrasta, kolektor będzie się włączał na dłużej; gdy wykona swoje zadanie i wielkoœć zajętego obszaru zmaleje, czas aktywacji będzie krótszy. Poniżej pewnego progu kolektor nie będzie w ogóle włączany. W drugiej krańcowej sytuacji, gdy cała pamięć będzie zajęta, współprogram zbierania nieużytków przekształci się w pełny sekwencyjny kolektor “oznaczanie-wymiatanie”.
Dalszą poprawę mechanizmu zbierania nieużytków osiągnięto przez potraktowanie w specjalny sposób obiektów, które przetrwały dostatecznie dużo cykli kolektora. Jest to technika znana jako usuwanie generacji (generation scavenging). Dane doœwiadczalne pokazały, że takie obiekty przeżywają całą fazę wykonania. Technika usuwania generacji polega na tym, że obiekty długowieczne usuwa się na pewien czas ze zbioru obiektów podlegających znakowaniu, skracając w ten sposób proces detekcji.
Dzięki kombinacji opisanych technik narzut na szybkoœć wykonania, pochodzący od zbierania nieużytków, jest rzędu 10%. Mechanizm zbierania nieużytków można jawnie wyłączyć, pozostawiając `N' w odpowiednim wierszu wspomnianego wczeœniej pliku System Description File:
GARBAGE COLLECTION(N)
Zbieranie nieużytków może być włączane i wyłączane dynamicznie, jeżeli w programie umieœcimy instrukcje wywołania procedur collection_off i collection_on; w systemach interakcyjnych korzystne może być jawne przełączenie kolektora w tryb zbierania nieużytków przez wywołanie procedur collect i full_collect, np. w czasie oczekiwania na reakcję użytkownika. Wywołanie full_collect włącza kolektor na czas wystarczający na oznakowanie i zebranie wszystkich obiektów martwych; wywołanie collect aktywizuje kolektor tylko na czas, okreœlony przez system wykonawczy. Klasy, które używają tych procedur muszą dziedziczyć od klasy bibliotecznej MEMORY, w której są one zadeklarowane.
W klasie MEMORY jest także zadeklarowana procedura dispose, którą można zredefiniować w klasie dziedziczącej od MEMORY. Jednak nawet starannie zredefiniowanej procedury dispose powinno się używać jedynie w odniesieniu do używanych przez program zasobów zewnętrznych, takich jak np. deskryptory obiektów, elementy baz danych, zasoby systemu okienkowego, etc.
4.11. Podsumowanie
Eiffel coraz wyraŸniej zajmuje pozycję wzorcowego, czysto obiektowego języka programowania, spychając z tej pozycji język Smalltalk. Dotyczy to szczególnie nauczania programowania: w bardzo wielu uniwersytetach uczy się programowania obiektowego na przykładzie konstrukcji języka Eiffel. W koncepcji języka dużo uwagi poœwięcono niezawodnoœci programów oraz prostocie, a przez to czytelnoœci składni, nawiązującej do języków Simula, Pascal i Ada. Eiffel posiada wszystkie charakterystyczne własnoœci nowoczesnych języków obiektowych: klasy, dziedziczenie pojedyncze i mnogie, polimorfizm, typizację statyczną i wiązanie dynamiczne, parametryzację klas, zdyscyplinowany mechanizm wyjątków, wbudowane narzędzia dla definiowania asercji, które promują programowanie przez kontrakt oraz klasy abstrakcyjne przydatne w szybkim prototypowaniu.
Zauważmy przy okazji, że w językach obiektowych, w których formalnie nie ma mechanizmu asercji, są one wprowadzane niejawnie jako dodatkowe instrukcje w ciałach metod. Jest to konieczne, ponieważ praktycznie wszystkie metody klas są funkcjami częœciowymi (Funkcja częœciowa jest to taka funkcja f: X!Y, która nie jest zdefiniowana dla wszystkich x"X, czyli jej dziedzina jest podzbiorem X).
Pewną wadą języka Eiffel jest istnienie dwóch trybów pamięci dla obiektów: odnoœnikowego (reference) i rozwiniętego (expanded). Wymusza to na programiœcie myœlenie o szczegółach implementacji już podczas projektowania klas; np. struktury listowe muszą być implementowane w trybie odnoœnikowym.
Największym chyba problemem jest ogromnie rozbudowana biblioteka klas, trudna do rozpoznania i poruszania się po niej; np. klasa definiująca listy jednokierunkowe ma ponad 50 operacji w publicznym intefejsie.
Poważnym problemem jest również brak łatwo dostępnego zintegrowanego œrodowiska programowego. Jednak już pojawiły się publicznie dostępne kompilatory podzbiorów języka Eiffel, jak np. SmallEiffel, dostępny w sieci Internet pod adresem http://www.loria.fr/SmallEiffel/. SmallEiffel jest produktem GNU, z kodem wynikowym w języku C lub B-kodem języka Java. Ten wariant kompilatora jest od 1995 roku używany przez wiele uniwersytetów, jako œrodowisko programowe dla nauczania programowania w językach obiektowych. Dostępne są wersje SmallEiffel na następujące platformy: HP-UX, IRIX, SCO, Solaris, VMS, GNU/Linux, MacOS, OS/2, Windows 95, Windows NT i MS DOS.
Innym miejscem, z którego można sprowadzić ogólnie dostępny kompilator języka Eiffel, jest http://www.elj.com/elj-win32. Eiffel ma swoją stronę domową we wspomnianej już firmie ISE (lokalizator http://www.eiffel.com); jej uniwersyteckim odpowiednikiem w Europie jest strona domowa o lokalizatorze http://www.cm.cf.ac.uk/CLE/, utrzymywana na serwerze University of Wales College of Cardiff. Wersje komercyjne języka Eiffel stwarzają możliwoœć pisania i wykonywania programów współbieżnych. Użytkownik może zdefiniować własną klasę, powiedzmy, SOME_CLASS, a następnie w deklaracji zmiennej odnoœnikowej poprzedzić nazwę klasy słowem kluczowym separate:
x: separate SOME_CLASS
Deklaracja taka oznacza, że x może zawierać odnoœnik (po !!x.make(...)) do nowego obiektu, przetwarzanego przez inny procesor (Eiffel definiuje procesor jako autonomiczny wątek sterowania, zdolny do sekwencyjnego wykonania instrukcji na jednym lub większej liczbie obiektów). Programy współbieżne komunikują się ze sprzętem za poœrednictwem specjalnego pliku Concurrency Control File (CCF). W pliku tym wymienia się węzły lokalne i zdalne z nazwami komputerów i aplikacji, w których będą tworzone obiekty w oddzielnych wątkach oraz nazwy serwerów, które pozwalają na dostęp do istniejących obiektów zewnętrznych.