Nach den Zuweisungen in Verbindung mit Ausdrücken für Zahlen- und Zeichenkettentypen stehen in diesem Artikel Vergleiche und logische Ausdrücke für die Ablaufsteuerung durch if, while & Co. sowie auch die Ablaufsteuerung selbst auf dem Programm, was dann noch durch die Ausnahmen (Exceptions) und ihre Behandlung ergänzt wird. Dabei tragen diese Inhalte zu einer weiteren Vervollständigung der kleinen CRM-Anwendung bei.

Wenden wir uns zuerst der Besprechung der Übung aus dem letzten Artikel zu, an die wir anschließend mit den Inhalten zur Anwendungslogik anknüpfen können.

Übung des vorigen Artikels:

  1. Erstellen Sie ein allgemeines EGL-Projekt EGLAllgemein und in diesem das Paket math.
  2. Erstellen Sie im Paket math des Projektes EGLAllgemein die Library Verteilung.
  3. Erstellen Sie in math.Verteilung die Funktion mittelWert(), die aus einem übergebenen Array von float-Zahlen den Mittelwert nach der Formel
    m = (x1 + x2 +   + xn)/n
    berechnet und als float-Zahl zurückgibt.
  4. Erstellen Sie in math.Verteilung die Funktion standardAbweichung(), die aus einem übergebenen Array von float-Zahlen die Standardabweichung nach der Formel
    s = ((x1-m)2 + (x2-m)2 + … + (xn-m)2)/n)1/2
    berechnet und als float-Zahl zurückgibt.
  5. Testen Sie beide Funktionen mit einem geeigneten Testprogramm TestVerteilung im gleichen Paket.
  6. Nehmen Sie das Projekt EGLAllgemein in den EGL-Erstellungspfad des Projektes EGLKunden auf.
    Kontextmenü von EGLKunden > Eigenschaften

  7. Nehmen Sie das Projekt EGLAllgemein analog auch in den Java-Erstellungspfad auf.
  8. Erstellen Sie im Paket eglKunden.logik des Projektes EGLKunden die Library Fristen mit der Funktion zahlungsDauer().Diese
    • empfängt einen Input-Parameter gruppe vom Typ string, mit dem (noch) nichts gemacht wird.
    • definiert die Variable zahlungsZiel als Array mit z. B. fünf Elementen vom Typ decimal(8,0) und initialisiert ihn mit sinnvollen Dezimal-Datumswerten der Form yyyyMMdd;
    • definiert eine "gleiche" Variable zahlung mit zu zahlunsZiel passenden Datumswerten;
    • berechnet die Zeitintervalle der Differenzen (zahlung[i] – zahlungsZiel[i]) und schreibt sie in ein genauso langes int-Array;
    • gibt das int-Array als Funktionswert zurück.
  9. Erstellen Sie im Paket eglKunden des Projektes EGLKunden das Programm ZahlungsAuswertung, das die Funktion zahlungsDauer() aufruft, das int-Array empfängt sowie anschließend die Funktionen mittelWert() und standardAbweichung() aufruft und deren Rückgabewerte auf Konsole ausgibt.

Diese Übung demonstriert die bewusste Wahl der Datentypen entsprechend den Gegebenheiten. So müssen für die statistischen Auswertungen von Datumsdaten im Dezimalformat und die dazu notwendigen Berechnungen tatsächlich die Datentypen nacheinander migriert werden: decimal > string > timeStamp > interval > int > float.

Die Lösung für eine Library mit den in den Punkten 1 bis 4 beschriebenen Funktionen und das Testprogramm könnten zu einer Lösung wie auf den Bildern 1 und 2 führen.

Bild 1: Die Library Verteilung

Zur Library Verteilung (Bild 1):

Die Funktion mittelWert() in den Zeilen 5 bis 11 gibt einen float-Wert zurück. Dieser muss, wenn nicht durch einen komplexen Ausdruck im return anonym erzeugt, als lokale Variable vom Typ float angelegt werden (Zeile 6). Diese wird auch gleich passend explizit initialisiert. Ohne explizite Initialisierung wäre die Variable implizit mit dem gleichen Wert initialisiert worden, der Code wäre jedoch nicht so transparent.

Zeile 8 gibt ein Beispiel einer häufig vorkommenden kumulierenden Zuweisung durch einen numerischen Ausdruck. Bei diesem haben das Ergebnis und beide Operanden idealerweise den gleichen Typ, so dass die Operator "+" natürlich auch auf dem Definitionsbereich float agiert und auch die Zuweisung zum Ergebnis im gleichen Typ bleibt. Diese Kumulationsanweisung hätte, entsprechend dem weggelassenen Faktor 2 im RPG, auch wie in Zeile 17 geschrieben werden können.

Der Typ des Divisionsausdrucks in Zeile 10 ist nicht ganz so einfach zu ermitteln. Hier bestimmt der "ranghöhere" Typ float den Typ des Ausdrucks, der idealerweise auch wieder genau zum Typ des Ergebnisses passt.

Bei der Funktion standardAbweichung() wird zur Vermeidung von redundantem Code die Funktion mittelWert() aufgerufen (Zeile 15).

Der Ausdruck in Zeile 17 hat drei Schachtelungsebenen, wobei eine im Kumulationsoperator versteckt ist. Der innere Ausdruck ist wie oben vom Typ Gleitkomma, so dass das Quadrieren als mittlere Operation auch in float stattfindet wie auch die äußere Addition und die Zuweisung.

Im return von Zeile 19 steckt erst einmal außen eine Zuweisung zur Rückgabe vom Typ float. Ansonsten arbeitet der Ausdruck wie der in Zeile 17. Das als Potenzieren durchgeführte Wurzelziehen hätte auch durch die Funktion MathLib.sqrt() durchgeführt werden können.

Bild 2: Das Test-Programm TestVerteilung

Zum Testprogramm TestVerteilung in Bild 2:

In der Programmvariablen intWerte wird das Array durch die fünf Initialwerte implizit mit fünf Elementen angelegt.

Das float-Array ist notwendig, denn von der Parameterliste der aufzurufenden Funktionen wird der Typ float[] erwartet. Der Typ int[] ist nicht zuweisungskompatibel zu float[], obwohl int zuweisungskompatibel zu float ist. Also muss man die Werte einzeln zuweisen, was man sinnvollerweise in einer for-Schleife (Zeilen 9 bis 11) erledigt.

In den Zeilen 12 und 13 passiert Folgendes: Die Funktion writeStdout() verlangt einen Zeichenkettentyp. Diesen bekommt man, indem der erste Operand ein String-Literal ist, das einen String-Kontext eröffnet und so mit dem überladenen String-Operator "+" die Verkettung auswählt, und nicht ein Additions- "+". Der damit festliegende Ausdruckstyp string führt dazu, dass weitere Operanden wie das float-Ergebnis des Aufrufs der Funktion mittelWert() durch implizite Anwendung der Library-Funktionen StrLib.formatNumber() in einen String konvertiert werden. Dies ist auch noch ein Beispiel für einen geschachtelten Funktionsaufruf innerhalb eines Ausdrucks, wobei dieser Ausdruck effektiv drei Schachtelungsebenen hat.

Die Ausgabe in den Zeilen 14 und 15 unterscheidet sich von der vorigen nur durch den "stärkeren" Verkettungsoperator "::", der genauso funktioniert hätte, wenn der erste Operand kein String gewesen wäre.

Bild 3: Die Library Fristen

Zur Library Fristen:

Der Input-Parameter gruppe ist später einmal dafür vorgesehen, ein Kriterium zu beinhalten, nach dem die Gruppe von Aufträgen aus dem ERP-System mit den statistisch auszuwertenden Zahlungsdaten ausgewählt werden soll.

Die lokalen Variablen zahlungsZiele und zahlungen simulieren die Datumsdaten von der zu betrachtenden Gruppe von Aufträgen.

Das Zeitintervall wird für eine vierstellige Anzahl von Tagen angelegt (Zeile 10). Bei einer zweistelligen Anzahl wären 100 Tage "lautlos" zu 0 Tagen geschrumpft.

Die Variablen zahlungsZiel und zahlungen vom Typ timeStamp in den Zeilen 11 und 12 werden zum Konvertieren in den Zeilen 17 und 18 benötigt und haben eine interne Maske entsprechend der dezimalen Zahlendarstellung des Datums im ERP-System.

Der "Zwischentyp" int[] für tageArray (Zeile 14) wird deswegen benötigt, weil eine Typmigration von interval nach float, worin die Statistik-Berechnungen durchgeführt werden, direkt, so wie in Zeile 20, nicht möglich ist. Dort wird die interval-Variable Tage im Argument von appendElement() in ein int migriert, d.h. konvertiert.

Bild 4: Das Statistik-Auswertungsprogramm ZahlungsAuswertung

Dieses Programm unterscheidet sich vom Test-Programm TestVerteilung dadurch, dass der Input für die Statistik-Auswertung durch die Funktion Fristen.zahlungsDauer() geliefert wird und die Elementeanzahl des Arrays erst zur Ausführungszeit bekannt ist. Deshalb wird bei der Erzeugung des als Argument an die Statistik-Funktionen zu übergebenden Arrays dieses in Zeile 10 dynamisch (mit 0 Elementen) angelegt und mit so vielen Elementen erweitert, wie durch die Funktion zahlungsDauer() aktuell geliefert werden. Beim Erweitern des Arrays durch Elemente in Zeile 12 wird der int-Wert eines Elementes in einen float-Wert migriert.

Vergleiche, Bedingungen und logische Ausdrücke

Die Anweisungen des EGL-Kerns, die keine Zuweisungen haben, haben im Wesentlichen mit Ablaufsteuerung der Anwendung zu tun, sowohl im Groben beim Aufruf von Programmen oder Funktionen als auch innerhalb der Funktionen zur bedingten oder zyklischen Ausführung von Anweisungen. Letztere, z. B. if und while, enthalten Ausdrücke vom Typ boolean, genannt logische Ausdrücke, die meist aus Vergleichen und ggf. logischen Operatoren bestehen. Auch logische Ausdrücke können zu Variablen des Typs boolean zugewiesen werden, und auch hier stecken in komplexen Ausdrücken wieder Zuweisungen. Setzen wir also die Betrachtungen zu Variablen, Typen und Ausdrücken mit denen zur Ablaufsteuerung fort.

Der elementare Bestandteil eines logischen Ausdrucks ist hauptsächlich der Vergleich, z. B.

zahl >=10 oder (upper(zuName) like "SCHMI%") oder (0 in meinArray).

Dieser besteht aus zwei Operanden, die durch den Vergleichsoperator und ggf. Leerzeichen getrennt sind. Die Operanden des Vergleichs sind Ausdrücke in einem bestimmten Typ, d. h. Variablen, Konstanten, Literale, Array-Elemente oder komplexe Ausdrücke. Der Vergleichsausdruck selbst hat den Typ boolean und hat somit als Werte entweder true oder false. Neben Vergleichen kann ein logischer Ausdruck auch noch Operanden in Gestalt von Variablen, Konstanten und Literalen vom Typ boolean enthalten.

Die Vergleichsoperanden sollten möglichst genau den gleichen Typ haben (gewisse Unterschiede mit entsprechenden Annahmen von EGL sind möglich), der dann auch den Vergleich steuert. Bei numerischen Typen orientiert sich dieser am sogenannten Zahlenstrahl, bei Datumsdaten an der "Zeitachse" und bei Zeichenkettentypen an der für den Ausdruckstyp definierten Sortierung.

Die logische Negation ("!") kehrt den Wahrheitswert eines logischen (Teil-) Ausdrucks um.

Das "logische und" gibt es in zwei Varianten, einer intelligenten (&& bzw. and), die schon bei nicht erfülltem ersten Operanden aufhört zu operieren, und einer auf den internen Darstellungen bis zum Ende arbeitenden (&). Der Operator && kann z. B. in Verbindung mit Nullwerten vor einer Exception bewahren. Entsprechend arbeitet das "logische oder" (|| , or bzw. |).

Weitere spezielle logische Operationen wie das exklusive oder lassen sich nach den Bool’schen Gesetzen aus den v. g. Operationen herleiten.

Anweisungen zur  "Grobsteuerung" der Anwendung

Einige Befehle des EGL-Kerns steuern den Ablauf von Anwendungen. Sie navigieren zwischen ihren Teilen bzw. führen die Kommunikation mit Fremdanwendungen oder Anwendungen aus anderen Technologien durch:

call: Aufruf eines EGL oder anderen Programmes,

transfer: Aufruf eines anderen Programmes mit Beendigung des laufenden Programmes,

forward: Gehen zu einer Website.

Für die Navigation zwischen den Funktionen einer Anwendung gibt es die folgenden Befehle:

Aufruf einer Funktion:

library.function(arg1, …),

return: Befehl zur Rückkehr aus einer Funktion, der bei Werte-Rückgabe unbedingt angegeben werden sollte.

Der Aufruf einer Funktion eines lokalen oder externen Service (Web Service) ist syntaktisch ähnlich dem Aufruf einer Bibliotheksfunktion, unterscheidet sich jedoch durch die Konfiguration im Deployment Deskriptor.

Anweisungen und Strukturen zur Logik-Steuerung innerhalb von Funktionen

Diese Anweisungen steuern die bedingte und zyklische Abarbeitung anderer Anweisungen oder Logik-Konstrukte gemäß der Strukturierten Programmierung. Diese machen einen goTo-Befehl, den es in EGL aus Kompatibilitätsgründen auch gibt, entbehrlich. Dieses Befehls-Urgestein wird allgemein geächtet und kann auch nur (noch) innerhalb von Programmen benutzt werden.

Die bedingte Verarbeitung if– oder als Zweiweg-Alternative gibt es als

if(bedingung)
// Verarbeitung bei Erfülltsein
else
// Verarbeitung bei Nichterfülltsein
end

oder ohne else als

if(bedingung)
// Verarbeitung bei Erfülltsein
end

Die in den meisten Sprachen vorhandene Mehrweg-Alternative gibt es in zwei Typen, einmal als case mit Schalter-Verzweigung und einmal mit beliebigen Bedingungen. Letztere hat folgende Gestalt:

case
when(x == 1)
// Verarbeitung bei x==1
when(x == 2 || x == 3)
// Verarbeitung bei x==2 oder x==3
otherwise
// Verarbeitung im sonstigen Falle
end

Auch hier kann der otherwise-Zweig entfallen, doch widerspricht dies sowohl einem guten Programmierstil als auch allgemeinen Qualitätsregeln.

Ist x eine Variable, die ein Verzweigungskriterium darstellt, kann man diese "ausklammern", woraufhin sich die Mehrweg-Alternative von oben, wenn man es so sehen will, vereinfacht:

case (x)
when(1)
// Verarbeitung bei x==1
when(2,3)
// Verarbeitung bei x==2 oder x==3
otherwise
// Verarbeitung im sonstigen Falle
end

Die while-Schleife, auch abweisende oder kopfgesteuerte Schleife genannt, hat folgende Gestalt:

while(durchführungsBedingung)
// Verarbeitung bei Erfülltsein
end

Eine fußgesteuerte oder nichtabweisende Schleife (DOUNTIL-Schleife, im RPG DOW) gibt es im EGL nicht. Der Vorteil bei Existenz eines solchen Logik-Konstrukts durch Einsparung von ein wenig Code gegenüber while in relativ wenigen Fällen würde sich mit dem Nachteil Ihrer Existenz an sich für die Sprache etwa aufheben.

Da bringt die for-Schleife als while-Schleife mit einer Lauf-Variablen schon echten Gewinn, insbesondere bei der Verarbeitung von Arrays.

for (i int from 1 to meinArray.getSize() by 1)
// Anweisungen i. A. mit meinArray[i]
end

oder rückwärts

for (i int from meinArray.getSize() to 1 decrement by 1)
// Anweisungen i. A. mit meinArray[i]
end

Es gibt auch noch eine weitere Variante der for-Schleife, die forEach-Schleife, die allerdings nur mit SQL-Ergebnismengen arbeitet und deshalb später in Verbindung mit dem Zugriff auf Datenbanken behandelt wird.

Wenn man die reine Lehre der strukturierten Programmierung etwas aufweicht, kann man zur Erleichterung der Programmierung und auch, wenn man es so sehen möchte, zur Vereinfachung des Codes "gezähmte" goTo-Befehle in der Ausprägung eines exit(im RPG LEAVE) oder continue (im RPG ITER) zulassen, wie von EGL auch getan. Während letzterer Befehl als Beendigung eines Schleifendurchlaufes höchstens marginalen Gewinn bringt, ist exit da schon potenter. Dieser Befehl kann sogar mit einem Mal mehrere Strukturen vorzeitig beenden, indem man das Struktur-Schlüsselwort als Operanden angibt, woraufhin die von innen nächste solche Struktur beendet wird:

for (i int from 1 to meinArray.getSize() by 1)
// Anweisungen i. A. mit meinArray[i]
if(vorzeitigesEnde)
exit for;
end
end

Die Logik der Ausnahmenbehandlung

Im Sinne einer orthodoxen strukturierten Programmierung ließe sich theoretisch auch eine Ausnahmenbehandlung mit den v. g. Logik-Konstrukten darstellen, doch wäre der entstehende Code i. Allg. sehr umfänglich und unübersichtlich. So hat EGL eine Ausnahmenbehandlung, wie sie die meisten Programmiersprachen haben. Selbst RPG IV verfügt inzwischen mit der MONITOR-Struktur über eine akzeptable Ausnahmenbehandlung, die der von EGL sehr ähnlich ist. EGL selbst benutzt ein vergröbertes Abbild der Java-Fehlerbehandlung, in die ja durch die Generierung die EGL-Fehlerbehandlung überführt werden können muss.

Das Kern-EGL definiert etwa ein Dutzend sogenannter Ausnahmen als Records vom Stereotyp Exception, womit diese "automatisch" zwei Variablen, messageID und message, besitzen. Diese Ausnahmen werden bei Auftreten entsprechender Situationen aus den Funktionen der EGL Libraries oder formal auch aus EGL-Befehlen geworfen. Beispiele dieser Ausnahmen sind FileIOException, IndexOutOfBoundsException, NullValueException, RuntimeException und TypeCastException, die nicht nur der Java Programmierer meist sofort zuzuordnen weiß.

Anders als bei Java werden Ausnahmen, die im EGL-Code explizit oder implizit durch Ausnahmen-belastete Funktionen geworfen werden, "automatisch" propagiert, wenn sie nicht durch die nachfolgend beschriebenen Code-Konstrukte gefangen werden. Bei einem Propagieren in einer Funktion wird das Ausnahmen-Problem an die aufrufende Funktion delegiert, die wieder implizit propagiert, sofern sie die Ausnahme nicht selbst explizit abfängt. An der Hülle eines Programmes oder Service ist Schluss mit einem bis jetzt beschriebenen Propagieren, dort wird die Ausnahme in eine InvocationException respektive ServiceInvocationException "eingepackt" – und in der Verpackung geworfen, aus der sie die aufrufenden Prozesse nach einem Fangen wieder auspacken können.

Das Werfen erfolgt mit dem EGL-Schlüsselwort throw, mit dem in Anwendersoftware bevorzugt eigene Ausnahmen geworfen werden, die als die besagten speziellen Exception-Records in EGL-Quellen definiert werden.

Das folgende Beispiel soll das Fangen und ggf. erneute Werfen von Ausnahmen demonstrieren:

try
    eingabeSatz = Eingabe.lesen();
onException(ex UngültigeDatenException)
    SysLib.writeStdout(ex.messageID :: ": " :: ex.message);
    // spezielle Behandlung UngültigeDatenException
onException(ex SQLException)
    SysLib.writeStdout(ex.messageID :: ": " :: ex.message);
    // Fortsetzung der speziellen Behandlung der SQLException
onException(ex AnyException)
    // Registrieren der unerwarteten Ausnahme
    throw ex;
end

Bei einer Dateneingabe können in der Funktion Eingabe.lesen() Ausnahmen geworfen werden. Der Funktionsaufruf wird durch das obige try-Konstrukt hinsichtlich geworfener Ausnahmen überwacht.

Die erste Ausnahme des Beispiels ist eine eigene, selbst definierte. Sie enthält als "Erbmasse" von EGL die beiden auf die Konsole ausgegebenen Variablen, wie sie auch die EGL SQLException enthält. AnyException ist eine allgemeine Ausnahme, zu der auch alle andern EGL-Ausnahmen gehören, so dass irgendwelche Ausnahmen in einem solchen onException gefangen werden, wenn sie nicht schon durch ein voranstehendes onException gefangen wurden. Somit wirkt das letzte onException als Universalfänger, weswegen er auch letzter Fänger sein muss. Für AnyException– Ausnahmen gibt es im Beispiel ein wenig Behandlung, doch soll nicht wie bei den vorher gefangenen Ausnahmen nach der Behandlung wieder heile Welt herrschen, sondern die Ausnahme soll original wieder geworfen werden, womit sie dann durch die umgebende Funktion propagiert wird.

Nur beim Durchlaufen der try-Sequenz bis zu deren Ende ohne Auftreten einer Ausnahme sind nach dem abschließenden end alle Aktionen durchgeführt worden. Andernfalls sind die Aktionen des try ab einschließlich der Aktion, aus der die Ausnahme kommt, i. Allg. nicht oder nicht vollständig abgearbeitet worden. Beim Behandeln im letzten onException wird noch nicht einmal nach dem end fortgesetzt, weil sogar die Ausführung der gesamten lfd. Funktion durch das throw suspendiert wird.

Das Konstrukt onException hat formal eine eingeschränkte Parameterliste mit nur einem Parameter, der überdies auch noch vom Typ eines Exception-Record sein muss. Dieser Parameter hat im Beispiel vereinfachenderweise jeweils bei allen Konstrukten den gleichen Namen. Die damit implizit deklarierten lokalen Variablen sind, obwohl namensgleich, trotzdem untereinander verschieden und gelten jeweils nur in dem nachfolgenden (impliziten) Anweisungsblock, d. h. bis zum nächsten onException bzw. end.

Übung zum Artikel

Die Library math.Verteilung soll um zwei Funktionen ergänzt werden, die beide vorhandenen Funktionen mittelWert() und standardAbweichung() um eine Kontrolle aller im Array übergebenen Werte auf >=0.0 erweitern. Beide Funktionen heißen genauso wie die vorhandenen, haben jedoch einen zusätzlichen Input-Parameter namens allesPositiv vom Typ boolean, der im Falle true die Kontrolle durchführen soll.

Die Funktionen sollen beim ersten Entdecken eines negativen Wertes eine Ausnahme UngültigeDatenException werfen, die in einem Paket exceptions des Projektes EGLAllgemein als Exception-Record zu definieren ist. Die Ausnahme soll auch ein Feld fehlerInfo vom Typ string besitzen, die eine Information über den zum Abbruch führenden Wert enthält. Die Ausnahme soll die ID "TKL0001" und die Nachricht "Abbruch wegen ungültiger Daten: " besitzen.

Beim Codieren der beiden Funktionen soll möglichst wenig redundanter Code entstehen.

Die beiden neuen Funktionen sollen im Programm ZahlungsAuswertung aufgerufen werden, jeweils mit und ohne Kontrolle, wobei die Ausnahme abgefangen und ggf. eine Fehlermeldung auf die Konsole ausgegeben werden soll.

Lösungshinweise:

  1. Zuerst neues Paket exceptions anlegen.
  2. Im neuen Paket neue Quelle mit dem Namen des Ausnahme-Records anlegen.
  3. Record editieren:
    • rec  eingeben und die Record-Definition durch die Inhaltshilfe vervollständigen lassen;
    • Record und schon generierte Variable umbenennen.
  4. Quelle sichern und generieren.
  5. Library Verteilung editieren:
    • Beide Funktionen kopieren und bei den Duplikaten den zusätzlichen Parameter einfügen;
    • in der neuen Funktion mittelWert() in der Schleife folgenden neuen Code einfügen:
      if (positiv && werte[i] < 0.0)
          ausnahme UngültigeDatenException = new UngültigeDatenException;
          ausnahme.messageID = "TKL0001";
          ausnahme.message = "Abbruch wegen ungültiger Daten: ";
          ausnahme.fehlerInfo = i ::
              ". Wert des Array hat unzulässigen Wert ::  werte[i];
          throw ausnahme;
       end
    • Import realisieren (z. B. durch das Verwalten der Importe über das Kontextmenü der Quelle);
    • in der neuen Funktion standardAbweichung() die Funktion mittelWert() mit zwei Argumenten aufrufen;
    • in den alten Funktionen statt der ursprünglichen Anweisungen die neuen Funktionen mit false als 2. Argument aufrufen.
  6. Einfügen von Aufrufen der neuen Funktionen in das Programm ZahlungsAuswertung.
  7. Alles sichern und generieren.
  8. Falls keine Ausnahme geworfen wird, in Fristen.zahlungsDauer() ein Zahlungsdatum vor das entsprechende Zahlungsziel legen.

Ausblick auf den nächsten Artikel

Im nächsten Artikel werden EGL Services und ihr Unterschied zu Libraries behandelt. Dazu werden statt Funktionen aus einer Library die gleichen Funktionen aus einem lokalen Service aufgerufen.