Objektorientierte Erweiterungen für Cobol, Teil 2

OOP - Eine Technologie auch für Cobol-Entwickler

06.07.1990

Objektorientierte Programmierung (OOP) ist in. Nur die Cobol-Anbieter haben sich von diesem Technologie-Boom bisher vornehm distanziert. Dabei wird in den Vereinigten Staaten längst darüber diskutiert, wie man die Vorteile von OOP in die führende Sprache für kommerzielle Anwendungen einbinden kann. Ken Belcher schlägt Spracherweiterungen vor, die zu einem objektorientierten Cobol führen sollen.

Bevor wir uns dem Problem einer Nachfolge (Inheritance) von Object-classes stellen, wollen wir die Cobol-Funktionen betrachten, die für einfache Object-instances erlaubt sind. Gibt eine Object-class an, daß ein normales Cobol-Datum ein PUBLIC-instance-Datum sein soll, erlaubt die Instance-Modifikation den Zugriff durch ganz normale Cobol-Anweisungen auf dieses Datum. Die Datenart wird durch die rechte (innere) Instance-Modifikation angegeben. Handelt es sich um ein Datenelement, sind die seiner Klasse und Kategorie entsprechenden Funktionen erlaubt. Auf ähnliche Weise sind Datengruppen zugelassen. Da wir jedoch andere Daten als PUBLIC erlauben (in einem nur durch Pointer zugewiesenen Objekt), sollten wir eine Tabelle von Objekt-Pointern haben. Das ist sicherlich eine Erweiterung der Sprache.

Wie bereits erwähnt, können wir mehr als einen Objekt-Pointer auf einen gegebenen Objectinstance haben: SET Objekt-Pointer-Name-1 TO Object-Pointer-Name-2.

Für die bis jetzt eingeführten Zusätze müssen beide Pointer für dieselbe Object-class definiert sein. Wir können auch einen Objekt-Pointer auf einen statischen Object-instance erhalten, indem wir das neue spezielle Register benutzen: SET Objekt-Pointer-Name TO Pointer TO Object-instance-Name-2.

Hier muß der Objekt-Pointer-Name als ein Pointer für dieselbe Object-class definiert sein, aus der der Object-instance-Name kommt. Ähnlich können wir die Werte zweier Pointer vergleichen (allerdings nur auf Gleichheit): IF Objekt-Pointer-Name-1 IS EQUAL TO Objekt-

Pointer-Name-2 OR Pointer TO Object-instance-Name IS NOT EQUAL TO Objekt-Pointer-Name.

Es sind die Einschränkungen für die Pointer-Definition zu beachten. Statische Object-instances können auch Operanden von MOVE, IF und BY CONTENT-Parametern von CALL sein:

MOVE Object-instance-Name-1 TO Object-instance-Name-2.

IF Object-instance-Name-1 IS EQUAL TO Object-instance- Name-2.

CALL Programm-1 USING BY CONTENT Object-instance- Name.

CALL Programm-2 USING BY CONTENT Pointer TO Object- instance-Name.

Die Object-class jedes Datenelementes und die USING-Eintragung der Procedure Division müssen übereinstimmen. Für Pointer, deren Object-class keine Einschränkungen hat, läßt das spezielle Register INSTANCE OF ähnliche Funktionen zu:

MOVE INSTANCE OF Objekt-Pointer-Name-1 TO INSTANCE OF Objekt-Pointer-Name-2.

IF Object-instance-Name IS EQUAL TO INSTANCE OF Objekt- Pointer-Name.

CALL Programm-1 USING BY CONTENT INSTANCE OF Objekt-Pointer-Name.

CALL Programm-2 USING BY CONTENT Objekt-Pointer- Name.

Und wir können selbstverständlich Object-instances oder Objekt-Pointer BY REFERENCE übergeben:

CALL Programm-3 USING BY REFERENCE Object-instance- Name.

CALL Programm-4 USING BY REFERENCE Objekt-Pointer- Name.

Hier gilt wie immer der Vorbehalt, daß Parameterart und Zielerwartung übereinstimmen müssen. Diese Zusammenstellung der Pointer-Arten ist allerdings nicht vollständig.

Verbindung von alten und neuen Funktionen

Die oben beschriebenen Zusätze mögen interessant und nützlich sein, berühren aber den Hauptzusatz von OOP nicht, die Code-Erweiterbarkeit. Nach wie vor fehlt allerdings die Möglichkeit, eine oder mehrere existierende Object-class-Definitionen zu nehmen und eine neue Object-class mit einer Kombination alter und neuer Funktionen zu erzeugen. Dafür ist eine zusätzliche Klausel im PROGRAM-ID-Eintrag für eine Object-class zuständig: FROM ancestor-Eintrag-1 [ALSO ancestor-Eintrag-2]...

Ein ancestor-Eintrag hat das folgende Format:

Object-class-Name [USING {Datenname-1}...]

[PRIVATE {Public-Name-1}...]

[RENAME {Public-Name-2 TO Public-Name-3}...]

[VALUE OF {Instance-Datenname IS Literal}... ].

Auch für Inheritance gibt es keine wesentlichen Einschränkungen. Im einzelnen kann eine Object-class mit mehreren Vorfahren direkt oder indirekt so oft wie erforderlich denselben Ahnen (Ancestor) beerben (inherit). Wenn das geschieht, wählt die Instance-Modifikation (qualifiziert durch den Object-class-Namen) den erforderlichen Instance aus.

Eine Erweiterung der SET-Anweisung gestattet es, mehrere Instances eines Vaters in seinem Sohn so zu durchlaufen, wie sie in der FROM-Klausel festgelegt wurden. Wurde keine ausdrückliche Instance-Auswahl getroffen, wird der erste (links) in der Abfolge angesprochen.

Die USING-Klausel legt fest, welche Parameter an die Initialisierungs-Routine des Vorfahren übergeben werden müssen, wenn die automatische Initialisierung aufgerufen wird. Das kommt nur dann vor, wenn der Sohn in der Procedure Division keinen Code für eine an ihn angepaßte Initialisierung zur Verfügung stellt, aber ein CALL für die Vererbungs-Initialisierung abgesetzt wird.

Diese nützliche Funktion läßt sich bequem für eine Untermenge von Elterngenerationen verwenden. Sie definieren einfach eine spezielle Nachfolgerklasse, die die Ahnen so bündelt, daß sie automatisch initialisiert (und/oder beendet) werden, deklarieren dann einen Instance dieses speziellen Nachfolgers und rufen während der Vererbungs-Initialisierung seine Initialisierungs-Routine auf.

Die PRIVATE-Klausel sorgt dafür, daß im Vorfahren als PUBLIC deklarierte Daten nicht notwendigerweise auch beim Sohn PUBLIC sind. So wird jeder Bezug auf diese Daten über den Object-class-Namen des Nachfolgers verhindert, nicht jedoch der Zugriff, wenn die Instance-Modifikation von einem Verweis benutzt wird, in dessen Namensumfang der Vorfahr enthalten ist und der Verweis seines Klassennamen als Kennzeichner benutzt.

Zugriffsrechte auf Vater und Sohn

Das ist nur vernünftig, da jeder Verweis dieser Art auf die entsprechenden Daten beim Vorfahren oder - über einen entsprechenden Pointer - beim Sohn zugreifen könnte. Solche Verweise können durch Angabe des Klassennamens vom Vorfahren der PRIVATE-Klausel entschärft werden. Sie verhindert, daß ein solcher Klassenname als Instance-modifier für den Verweis auf jeden direkten oder indirekten Instance des Nachfolgers benutzt wird. Da es die Rechte des Vorfahren verletzen würde, lassen wir nicht zu, daß ein ihm zugeordnetes PRIVATE-Datum PUBLIC gemacht wird.

Die RENAME-Klausel ändert den PUBLIC-Namen eines Ahnen in einen neuen PUBLIC-Namen, über den das Datum im Sohn angesprochen werden kann. Dadurch können Daten leichter aufgerufen werden.

Die VALUE-Klausel gestattet dem Sohn, den vom Vater nur für PUBLIC-Datennamen vergebenen Ausgangswert (falls vorhanden) außer Kraft zu setzen. Diese Klausel ist auch für Vereinbarungen über statische Object-instances vorgesehen. Der Vollständigkeit halber sollten wir auch für einen Pointer-bezogenen Object-instance eine solche Klausel zulassen. Damit würde die Initialisierung aktiviert, wenn ein neuer Instance für diesen Pointer zugewiesen würde.

Schließlich setz jede Method im Nachfolger (einschließlich lokaler Methods, die dieselbe Bezeichnung wie ein ererbter Method-Name mit dem PUBLIC- oder PRIVATE-Attribut tragen) die ererbte Method außer Kraft. Folglich ruft ein CALL, der vom Code im Vorfahren oder dazwischen liegenden Geschlechtern abgesetzt wurde, die neue Method auf. Lokale Nachfolger-methods verhindern ihr Außerkraftsetzen, wenn ihnen neue Söhnen und Enkel folgen. Es muß allerdings sichergestellt werden, daß Ahnenverweise weiterhin die neue lokale Method aufrufen.

Eine außer Kraft gesetzte PUBLIC-method des Vorfahren sorgt dafür, daß ein implizites PRIVATE für den method-Namen angenommen wird, das den eindeutigen Bezug auf den neuen, durch den neuen Object-class-Namen qualifizierten Method-Namen gestattet. Allerdings muß die Anzahl der Parameter, jeder Parameter und jedes Ergebnis identisch sein, da die neue Method von einem CALL in einem Ahnen aufgerufen wird, der als seinen impliziten Object-instance einen Nachfolger-Object-instance bearbeitet.

Die Bedingungen für Kompatibilität

Was bedeutet in diesem Zusammenhang Kompatibilität? Zunächst müssen wir festhalten, was ein Nachfolger-Object-instance enthält. Seine Instance-Daten umfassen sämtliche Informationen seines Ahns (in der Reihenfolge der FROM-Klausel). Darauf folgen die neu deklarierten Daten, die den Nachfolger selbst betreffen. Wir können folglich einen Pointer auf einen Instance jedes Vorfahrens erzeugen, indem wir auf seinen Instance im Nachfolger zeigen.

Dasselbe Verfahren kann auf alle Parameter und ein möglicherweise vorhandenes Ergebnis angewendet werden. Wenn eine Vorläufer-method einen CALL auf einen Method-Namen absetzt, den der Sohn außer Kraft gesetzt hat, muß der Pointer wieder zu einem Nachfolger-Object-Pointer konzertiert werden. Für den impliziten Method-object-instance ist das leicht möglich, wenn mit ihm der Nachfolger erzeugt wurde.

Spezifiziert jedoch ein Nachfolger-Method-Parameter einen entsprechend Object-instance, ist der Aufruf des Vorfahren mit der übergeordneten Method nur möglich, wenn der dieser Method einen Parameter übergibt, der mit dem Parameter des Sohns übereinstimmt. Das kann beim Kompilieren Probleme schaffen, die sich allerdings nicht von Regelverstößen bei den CALL-Parametern im bestehenden Cobol unterscheiden.

Ein weiterer Hinweis: Wenn eine Vorfahren-object-class die Pointer-Angabe hat, muß der Nachfolger den Ahn initialisieren. Der Grund: Der Nachfolger kann den Vorgänger-Object-instance nur durch einen Pointer darstellen, die Zuweisung eines Nachfolger-Object-instance muß dagegen die Zuweisungs-Method des Vorfahren aufrufen. Das gilt analog für den CANCEL-Befehl eines Nachfolger-Object-instance. Dieser hat immer einen CANCEL auf das Vorgänger-Object-instance abzusetzen. Die Object-class-Definitionen lautet folgendermaßen:

IDENTIFICATION DIVISION.

PROGRAM-ID. PGM1 IS COMMON Object-class RESERVE 1 AREA Pointer.

DATA DIVISION.

WORKING-STORAGE SECTION.

01 REC1 PRIVATE PIC X.

END PROGRAM PGM1.

IDENTIFICATION DIVISION.

PROGRAM-ID. PGM2A IS COMMON Object-class.

DATA DIVISION.

WORKING-STORAGE SECTION.

01 REC1 PRIVATE Pointer TO PGM1.

END PROGRAM PGM2A.

IDENTIFICATION DIVISION.

PROGRAM-ID. PGM2B IS COMMON Object-class FROM PGM1.

DATA DIVISION.

WORKING-STORAGE SECTION.

END PROGRAM PGM2B.

IDENTIFICATION DIVISION.

PROGRAM-ID. PGM3.

DATA DIVISION.

WORKING-STORAGE SECTION.

01 REC1 INSTANCE OF PGM2A.

01 REC2 INSTANCE OF PGM2B.

PROCEDURE DIVISION.

PAR1. CALL REC1.

CANCEL REC1.

CALL REC2.

CANCEL REC2.

END PROGRAM PGM3.

Der Fall einer separaten Kompilierung

Die oben besprochenen Zusätze stellen eine wesentliche Cobol-Erweiterung dar. Sie sind in der Lage, jedes Programm auszuführen, was zur Zeit in Cobol erstellbar ist. Allerdings eignen sie sich nur für komplexe Anwendungen, wenn ein Gesamtsystem auf einzeln kompilierbare Quelldateien heruntergebrochen werden kann. Zwar sind ein ganze Reihe hilfreicher Objektbibliotheken auf dem Markt, allerdings nicht in Cobol und meist auch ohne Quellcode.

Zum Beispiel OS/2: Es gibt Anwender, die OS/2- Anwendungsprogramm-Schnittstellen (APIs) aufrufen wollen, wofür diese Schnittstellen die PASCAL-Aufrufkonventionen benutzen und die Übergabe einiger Parameter mit Wert verlangen. Darüber hinaus ist die Anzahl der APIs beträchtlich, und es geht aus ihren Beschreibungen nicht hervor, daß sie Wert- und Adressen-Parameter verlangen.

Die Lösung kann hier heißen, Programm-Prototypen zur Verfügung zu stellen. Sie werden durch das reservierte Wort EXTERNAL (statt INITIAL, eine wenig sinnvolle Angabe für Programme, die in Wirklichkeit anderswo definiert sind) im PROGRAM-ID-Paragraphen festgelegt. Zusätzlich haben wir in diesem Fall die Kopfzeile der Procedure Division erweitert.

Jedem in der USING-Leiste angesprochenen Datenfeld kann ein BY CONTENT oder BY REFERENCE vorangehen, wobei der Default-Wert BY REFERENCE ist und beide Angaben sich - genau wie in der CALL-Anweisung - über die folgenden Parameter erstrekken. Hinsichtlich der Cobol-Aufrufkonventionen bedeutet BY CONTENT, daß eine unbeschädigte Kopie der Parameter übergeben werden muß.

BY REFERENCE heißt, daß das Zielprogramm die Übergabe der echten Daten erwartet, doch kann die CALL-Anweisung BY CONTENT für die entsprechenden Parameter benutzen, um sicherzugehen, daß die Parameter unverändert sind. Hinsichtlich der PASCAL-Aufrufkonventionen übergeben wir BY CONTENT-Daten als gestapelte Wert. Die Programm-Prototypen erlauben es uns, über die passende Aufrufkonvention zur Zeit der Kompilierung zu entscheiden und für Datenelemente im Zielprogramm Werte zu modifizieren und Literale zu übergeben.

Außerdem haben wir sowohl der CALL- als auch der Procedure-Division-Leiste eine GIVING-Bezeichner-Angabe zugefügt. Dieser Wert wird stets BY CONTENT vom Zielprogramm an das Programm zurückgegeben, das den CALL absetzt. Diese Verwendung von EXTERNAL deckt sich mit der Verwendung für Datei-Konnektoren und Sätze in Cobol. Sie beinhaltet die Verfügbarkeit eines Instance des Programms für alle anderen Programme der Objektprogramm-Einheit, die ihn beschreiben, wobei die Beschreibungen natürlich übereinstimmen (und mit der echten Definition des Programms verträglich sein) müssen.

Dieser Zusatz stellt genau das zur Verfügung, was wir für die separate Kompilierung einer Object-class benötigen: Das EXTERNAL-Attribut muß im PROGRAM-ID-Paragraphen der Object-class angegeben werden. Außerdem sind alle Instance-Daten und Methods wie üblich innerhalb der Object-class zu beschreiben; allerdings kann PRIVATE statt PUBLIC für alle Daten und Methods benutzt werden, auf die dieses Programm keinen Zugriff haben soll. Es müssen weder interne Daten noch Anweisungen der Procedure Division angegeben werden.

Außerdem ermöglicht dies unterschiedliche Views auf verschiedene Programme. Selbstverständlich sind Konzerndaten durch die Vergabe von Zugriffsberechtigungen seitens des Betriebssystems zu schützen, denn nichts hindert ein Programm daran, Object-instance-Daten wieder als PUBLIC zu deklarieren.

Die Gültigkeit von Pointern

Ein wichtiges, hier nicht behandeltes Thema ist die Gültigkeit von Pointern. Pointer auf nicht mehr existente Object-instances dürfen nicht für Verweise benutzt werden. Genauer gesagt: Pointer sollten nicht auf Object-instances zeigen, die einen kürzeren Lebenszyklus als die Pointer haben.

Nehmen wir als Beispiel einen Pointer auf einen statischen. Object-instance, der in der Working-Storage eines anderen Programms definiert ist und entweder das INITIAL-Attribut hat oder Ziel einer CANCEL-Anweisung ist. Ein solcher Pointer sollte nicht an ein aufrufendes oder ihn enthaltendes Programm übergeben werden. Ich bezweifle allerdings, daß wir das durch die Überwachung der Kompilierzeit vollständig verhindern können.

Dieses Problem taucht auch bei INDEX-Datenfeldern auf, die dazu mißbraucht werden können, einen INDEX-Name-Wert von einem INDEX für eine Tabelle an einen INDEX für eine andere Tabelle zu übergeben. Die in meinen Augen wünschenswerteste Einschränkung ist, daß Datensätze keine Pointer auf Daten enthalten sollten, die für die Lebenszeit der Objektprogramm-Einheit bestehen. Eine Ausnahme ergäbe sich jedoch, wenn die Datei am Ende der Objektprogramm-Einheit ebenfalls verworfen wird.

Ein weiterer Zusatz für Standard-Cobol bestünde darin, das Dateisystem zu einem integralen Bestandteil von OOPs zu machen, indem man einer Object-class die Angabe gestattet, daß sie in einer RELATIVE- oder INDEXED-Datei gespeichert wird. Dann wären symbolische Pointer auf solche Objekte zu benutzen, die in Sätzen derselben oder einer anderen Datei gespeichert werden. Solche Object-instances könnten gelesen und automatisch fortgeschrieben werden. Allerdings benötigen solche Objekte während der Objektprogramm-Einheit speicherbezogene Pointer.

Noch eine Erweiterung scheint mir wünschenswert, nämlich die OCCURS-Klausel als Angabe in einem Vorfahreneintrag zuzulassen. Damit könnte eine eindimensionale Tabelle über die Vererbung deklariert werden, was wegen der automatischen Zuweisung, Initialisierung und Löschung der Vorfahren-instance-Daten wichtig ist. Mehrdimensionale Tabellen können definiert werden, indem man Zwischen-Vorfahren erzeugt, die nur im echten Ziel-Nachfolger wirkliche Object-instances haben.

Um einen Konsens für objektorientierte Erweiterungen von Cobol nicht zu behindern, habe ich hier auf die Behandlung von echt dynamischen Zuweisungen sowie von Speicherbereinigungen verzichtet.