5                  Erfahrungen aus der Evolution des Micrologica-Telefonie-Frameworks

Die Beobachtungen und Erfahrungen aus der Entwicklung des Micrologica-Telefonie-Frame­works dienen als Grundlage meiner Überlegungen zur Framework-Evolution. Ich möchte daher in diesem Kapitel die wichtigsten Schritte in der bisherigen Evolution dieses Frameworks darstellen und einige allgemeine Folgerungen für das Design und die Konstruktion von Frameworks ableiten.

5.1          Unnötige Compilezeit-Abhängigkeiten

Nachdem das Framework eine gewisse Größe erreicht hatte, stellte sich heraus, daß viele Änderungen dadurch erschwert wurden, daß sie fast immer eine aufwendige Neucompilierung erforderten. Dabei war der resultierende Compilierungsaufwand weitgehend unabhängig von Art und Ort der Änderung - es wurde fast immer das gesamte Framework und die (zu diesem Zeit­punkt einzige) Anwendung neu übersetzt.

Eine Analyse der Abhängigkeiten der einzelnen Dateien ergab, daß tatsächlich fast alle auf irgendeine Weise direkt oder indirekt voneinander abhingen. Daraufhin wurde die Art der Abhängigkeiten untersucht. Es stellte sich heraus, daß auch diverse Dateien voneinander abhingen, die auf fachlicher Ebene voneinander völlig unabhängig waren. Nachdem diese Abhängigkeiten entfernt bzw. auf ein Minimum reduziert waren, sank der durchschnittliche Compilierungs­aufwand je Änderung auf etwa die Hälfte, so daß die zeitliche Auswirkung von Änderungen deutlich geringer wurde.

5.1.1      Arten von Abhängigkeiten

Die Abhängigkeiten zwischen Framework-Komponenten bestimmen die Auswirkungen von Änderungen. Man kann dabei zwischen zwei Arten von Abhängigkeiten unterscheiden: fachlich motivierte und technisch motivierte. Die fachlichen Abhängigkeiten spiegeln Abhängigkeiten innerhalb der modellierten Domäne wider. Änderungen an fachlichen Konzepten, auf denen andere Konzepte basieren, führen auch dort zu Konsequenzen.

Definition 22: Fachliche Abhängigkeiten

Abhängigkeiten, die "in der Natur" der modellierten Domäne liegen, bezeichnen wir als fachliche Abhängigkeiten.

Bei der Implementation werden die fachlichen Konzepte mit den in der jeweilig verwendeten Programmiersprache zur Verfügung stehenden Mitteln ausgedrückt. Dies hat Konsequenzen auf die bestehenden Abhängigkeiten. In C++ sind sprachbedingt auch einige Implementations­details (private/protected) in der Klassenschnittstelle zu sehen:

·      Benutzung anderer Klassen (#include)

·      private Klassenvariablen

·      protected Klassenvariablen

Diese Abhängigkeiten in den Header-Dateien (.hpp) führen dazu, daß das System häufiger compiliert werden muß als fachlich nötig. Übliche Generierungswerkzeuge wie z.B. make (siehe [Feldman 79]) prüfen lediglich das Dateidatum, ob sich abhängige Dateien verändert haben - nicht aber inhaltlich, ob eine Neucompilierung tatsächlich nötig ist.

Definition 23: Compilezeit-Abhängigkeit

Die Abhängigkeiten einzelner Compilierungseinheiten untereinander, die bei Änderung einer Einheit ebenfalls die Neucompilierung einer anderen Einheit erzwingen, werden als Compilezeit-Abhängigkeiten bezeichnet.

 

5.1.2      Reduktion der Abhängigkeiten

Die Compilezeit-Abhängigkeiten gehen oft weit über die fachlichen Abhängigkeiten hinaus. Daher sollten die Abhängigkeiten der Header-Dateien untereinander auf das absolut not­wendige Minimum beschränkt werden, um auf dem Framework basierende Anwendungen soweit wie möglich vor unnötigen Neucompilierungen zu schützen. Oftmals reicht es aus, die Include-Dateien von der Header-Datei in die Implementations-Datei (.cpp) zu verlagern.

 

gängige Praxis

weniger Abhängigkeiten

 

#include "MakeCall.hpp"

#include "AnswerCall.hpp"

//...

 

class ocRequestFactory {

public:

  ocMakeCall oNewMakeCall(...) =0;

  ocAnswerCall oNewAnswerCall(...) =0;

  // ...

};

 

class ocMakeCall;

class ocAnswerCall;

// ...

 

class ocRequestFactory {

public:

  ocMakeCall oNewMakeCall(...) =0;

  ocAnswerCall oNewAnswerCall(...) =0;

  // ...

};

Abbildung 24: Reduzierung der Abhängigkeiten

Das Beispiel in Abbildung 24 zeigt unnötige Abhängigkeiten: Ein Klient, der die Request-Fabrik zur Erzeugung von MakeCalls benutzt, würde bei der ersten Variante auch bei Änderungen an der Schnittstelle des AnswerCall neu compiliert werden. Bei der zweiten Variante müssen nur diejenigen Compilierungseinheiten neu übersetzt werden, die selbst AnswerCall.hpp einbinden, wie z.B. die Implementations-Datei der Request-Fabrik.

Die volle Tragweite dieser Umstellung (und der analogen Umstellung der konkreten ACL-RequestFactory) wird erst deutlich, wenn man die Zahl der Abhängigkeiten für einige Dateien gegenüber­stellt und dabei die indirekten Abhängigkeiten mitzählt:

 

 

 

vorher

nachher

 

Datei

abhängig von n Dateien bei 10 Request-Typen

abhängig von n Dateien bei 10 Request-Typen

 

RequestFactory.hpp

10

0

 

ACL-RequestFactory.hpp

21

1

 

ACL-RequestFactory.cpp

22

22

 

TSAPI-RequestKonverter.hpp (benutzt die RequestFactory)

11

1

Abbildung 25: Abhängige Dateien

Obwohl die Kopplung zwischen den Partitionen Telefonie-Protokoll (hier: TSAPI) und den Telefonie-Basis-Objekten über die abstrakte Oberklasse RequestFactory erfolgt, hätte vorher eine Änderung an den einzelnen Request-Typen zu einer Neucompilation des Request-Konverters geführt. Die Kapselung der Partition wurde jetzt somit deutlich verbessert. Es wird aber auch deutlich, daß sich die fachlichen Abhängigkeiten natürlich nicht reduzieren lassen, so daß die Implementierung der konkreten Fabrik weiterhin alle konkreten Request-Klassen kennen muß.

Es gibt in C++ nur 3 Fälle, in denen Header-Dateien in einer Klassendeklaration eingebunden werden sollten [Lakos 96, S. 379]:

1.    Subtyp-Beziehung: die Klassen erben voneinander

2.    Enthält-Beziehung: die Klasse enthält ein Exemplar (keinen Pointer) einer anderen Klasse (oder wird als Template-Parameter verwendet)

3.    Inline-Methoden: andere Klassen werden von Inline-Implementationen benötigt

Bei allen anderen Verwendungen (z.B. als Rückgabewert oder Parameter) ist das Einbinden der Header-Datei nicht notwendig.

Inline-Methoden sollten ohnehin vermieden werden, da Änderungen an ihrer Implementation unweigerlich zur Neucompilierung des Systems inklusive alle Framework-Klienten führt. Selbstverständlich sollte eine Header-Datei nur die Deklaration einer (!) Klasse enthalten.

Eine Nichtbeachtung dieses Prinzips kann bei großen Projekten dazu führen, daß alleine der Aufwand für die notwendigen Neucompilationen Änderungen unmöglich bzw. unpraktikabel macht.

Ein anderes Beispiel macht deutlich, daß leicht auch fachlich unabhängige Partitionen durch eine unbedachte Implementation miteinander verbunden sein können: Eine frühe Version der verwendeten String-Klasse verwendete zur Fehlerausgabe direkt Methoden aus dem IPC-Framework, was dazu führte, daß alle Partitionen, die Strings verwendeten, nur zusammen mit dem IPC-Framework getestet werden konnten. Die Aufhebung dieser Kopplung hat zwar zu einer kleinen Redundanz geführt, aber die Trennung der Partitionen wieder hergestellt.

5.1.3      Bessere Generierungswerkzeuge

Wie bereits erwähnt, betrachtet das gängige Generierungswerkzeug make lediglich das Datei­datum, um festzustellen, ob eine Neucompilation notwendig ist. Dies führt dazu, daß im Extrem­fall das Hinzufügen eines Leerzeichens oder eines Kommentars zur Neucompilation des gesamten Systems führt.

Hier könnte Abhilfe geschaffen werden durch ein Generierungswerkzeug, das zu jeder Datei eine Kopie erzeugt, die von Kommentaren befreit und mit einer Standard-Formatierung versehen wird. Nur wenn sich der Inhalt dieser Datei ändert, müßte eine Neucompilation der abhängigen Dateien ausgelöst werden. (Zur Erzeugung des Systems bzw. zur weiteren Bearbeitung würde selbstverständlich weiterhin die Originaldatei verwendet.)

Gerade das nachträgliche Einfügen von Kommentaren sollte nicht durch eine aufwendige Neu­compilation "bestraft" werden.

5.2          Umstellung des Meta-Object-Protocol

Die Requests (an die Tk-Anlage) sind in einer Vererbungshierarchie organisiert (siehe Abbildung 26). Über weite Strecken werden sie im System nur unter ihrer Oberklasse behandelt. Diese polymorphe Verwendung ist ein wichtiges Mittel, die einzelnen Bereiche zu entkoppeln. An einigen Stellen müssen sie jedoch unter ihrer vollen Schnittstelle bekannt sein, so daß ein sog. "down-cast" nötig ist, also eine Typkonvertierung von einer Oberklasse auf eine Unterklasse abhängig vom dynamischen Typ des Objekts.

Abbildung 26: Vererbungshierarchie der Requests

Zunächst wurde es als eine Ausnahme betrachtet, daß die dynamische Typinformation benötigt wird. Da die zu diesem Zeitpunkt verwendeten C++-Compiler keine Informationen über den dynamischen Typ zur Verfügung stellten, wurde in der Oberklasse aller Requests eine sondierende Funktion abstrakt implementiert, über die man eine String-Repräsentation des dynamischen Typs bekommen kann. Mit dieser konnte dann bei Bedarf der dynamische Typ ermittelt werden.

Im Laufe der Erweiterung des Frameworks ergab sich jedoch noch an diversen anderen Stellen ein Bedarf nach dynamischen Typinformationen, so daß ein allgemein verfügbarer Mechanismus benötigt wurde. So können beispielsweise Events (siehe Abbildung 27) in der virtuellen Tk-Anlage unter ihrer Oberklasse verwaltet und den Objekten des Half-Call-Modells zugeordnet werden - zur Auswertung muß jedoch ihr konkreter Typ bekannt sein.

 

Abbildung 27: Vererbungshierarchie der Events

Dieser Bedarf nach Meta-Informationen tritt offenbar recht häufig auf, so daß fast alle größeren Frameworks für Programmiersprachen ohne Zugriff auf Meta-Informationen einen solchen Mechanismus bereitstellen (z.B. ET++, Microsoft MFC etc.). Die Notwendigkeit, mit dem dynamischen Typ von Objekten zu arbeiten, resultiert aus dem Entwurf lose gekoppelter Systeme. Solange Objekte zwischen lose gekoppelten Bereichen nur in einer Richtung ausgetauscht werden, läßt sich das gewünschte Verhalten der polymorphen Objekte meist durch virtuelle Methoden erreichen. Sobald jedoch Objekte polymorph in beiden Richtungen ausgetauscht werden, entsteht sehr häufig der Bedarf, auf den konkreten Typ der Objekte zuzugreifen.

Allgemein werden Informationen über den Typ eines Objekts zur Laufzeit als Meta-Object-Protocol bezeichnet. Nach [Siberski 95] umfaßt dieser Oberbegriff vier Bereiche:

1.    Abfragen über die Klassenzugehörigkeit (Runtime Type Information)

2.    Abfragen über die Vererbungsbeziehung

3.    Abfragen über die Repräsentation des Objektzustandes

4.    Abfragen über die Schnittstelle

Im Telefonie-Framework wird (zumindest bisher) lediglich der erste Bereich benötigt.

Die verwendeten Compiler stellten überhaupt keine MOP-Unterstützung zur Verfügung. Es war jedoch abzusehen, daß der in Arbeit befindliche ANSI-Standard für C++ mindestens Runtime Type Information (RTTI) beinhalten würde, so daß in einiger Zeit die von uns benötigte Funktionalität in den meisten C++-Compilern enthalten sein würde (siehe [Stroustrup 94b, Seite 390ff], [Meyers 96, Seite 12ff]).

Es stellten sich somit hauptsächlich drei Fragen:

1.    Sollte das Framework auf einen allgemeinen MOP-Mechanismus umgestellt werden, obwohl sich die Umstellung auf alle Kundenklassen auswirken würde ?

2.    Wie geht man mit den zu erwartenden Änderungen im Sprachstandard um ? Sollte man versuchen jetzt eine Hilfskonstruktion zu implementieren, die in absehbarer Zeit überholt sein wird ?

3.    Wie kann die benötigte Untermenge der MOP-Funktionalität am einfachsten implementiert werden ?

Die Beantwortung der ersten Frage fiel am leichtesten. Da zu diesem Zeitpunkt kaum Kunden­klassen betroffen waren, war es einfach, sich für eine Umstellung zu entscheiden, insbesondere deshalb, weil mit einem weiteren Wachstum der betroffenen Vererbungs­hierarchien gerechnet wurde.

Der Diskussion der anderen beiden Fragen widmen sich die beiden folgenden Abschnitte.

5.2.1      Umgang mit kommenden Spracheigenschaften

Ganz allgemein stellt sich die Frage, wie mit absehbaren Änderungen in der Implementations­sprache umgegangen werden soll. Im Bezug auf C++ betrifft dies nicht nur das Meta-Object-Protocol, sondern auch Behälterklassen, String-Klasse oder Wahrheitswerte, deren Implementation vom ANSI-Standard geregelt wird.

Der laufende Standardisierungsprozeß von C++ ist noch nicht abgeschlossen. Der Abschluß des Standardisierungsverfahrens wird für 1998 erwartet. Es gibt jedoch bereits jetzt eine Reihe von Veröffentlichungen, die den zukünftigen ANSI/ISO-Standard recht genau beschreiben ([Ellis 90], [Stroustrup 94a bzw. b.], [Musser 96]).

Zur Zeit ist der Sprachumfang der verfügbaren C++-Compiler sehr unterschiedlich. Viele Compiler unterstützen erst eine Untermenge des geplanten Standards. Sowohl für den Entwickler von Anwendungen als auch für den Entwickler eines Frameworks stellt sich die Frage, wie er mit den neuen Eigenschaften von C++ umgeht:

a)    Wenn er sie intensiv verwendet, besteht die Gefahr, daß sein Code nicht mehr auf Compiler portabel ist, die diese Eigenschaft noch nicht unterstützen.

b)   Wenn er sich bemüht, sie nicht zu nutzen, muß er evtl. umfangreiche Hilfs­konstruktionen erstellen, um die benötigte Funktionalität zu erhalten.

Variante a) ist noch am ehesten zu akzeptieren, da im Laufe des Jahres 1997 wohl die meisten wichtigen Hersteller von C++-Compilern alle neuen Eigenschaften implementiert haben werden.

Es stellt sich jedoch die Frage, wie mit noch nicht verfügbaren Eigenschaften umgegangen werden soll.

Wichtigster Gesichtspunkt war es, Portabilität zu erreichen und nicht zu Änderungen am Quell­text gezwungen zu werden, sobald neue Compiler-Versionen erscheinen. Zum anderen sollte aber nicht zu viel Arbeit in die Implementation von Funktionalität gesteckt werden, die es in ein paar Monaten evtl. "frei Haus" gibt.

Dies hat zu zwei Designgrundsätzen für die Implementation zu erwartender Sprach­eigen­schaften geführt:

1.    Die Verwendung der Nachimplementierungen sollte den standardisierten Eigen­schaften semantisch so ähnlich wie möglich sein.

2.    Die Benennung muß sich lexikalisch unbedingt von der standardisierten Benennung unter­scheiden, um nicht mit ihr in Konflikt zu kommen.

Bei den Container-Klassen haben wir uns für die vom Compiler mitgelieferte Bibliothek entschieden, für die aus einem anderen Projekt bereits eine portable Implementation für andere Plattformen vorlag. Zu einem späteren Zeitpunkt wäre es möglich, das API der Klassen­bibliothek mit Hilfe der STL-Klassen aus dem C++-Standard zu implementieren.

Für die Realisierung eines Meta-Object-Protocols haben wir uns für den Einsatz einer Reihe von Makros entschieden, die in der Verwendung den RTTI-Konstrukten des C++-Standard entsprechen, jedoch nur eine Untermenge der Funktionalität implementieren.

Der hier vorgeschlagene Weg geht davon aus, daß die aktuelle Standardisierung der Sprache C++ kurz vor dem Abschluß steht. Die vorgestellte Methode, damit umzugehen, ist jedoch allgemeiner anwendbar, sofern Informationen über zukünftige Veränderungen der Sprache bzw. Implementations­umgebung vorliegen.

5.2.2      Implementation eines Meta-Object-Protocol

Wie bereits angedeutet, wurden diese Informationen zuerst explizit mit einer Abfragemethode in den jeweiligen Oberklassen realisiert, die von jeder Unterklasse überschrieben werden mußte. Dies ist allerdings ein für den Programmierer recht aufwendiges Verfahren und insbesondere bei mehrstufigen Vererbungshierarchien auch sehr fehlerträchtig. (Da die erste Vererbungs­stufe bereits die abstrakte Methode der Oberklasse redefiniert, findet bei weiteren Vererbungsstufen keine Warnung durch den Compiler mehr statt, wenn die Redefinition vergessen wird.)

Das Abbilden der Typinformationen auf Strings hatte den zusätzlichen Nachteil, daß zur Compile­zeit keine Prüfung auf die richtige Schreibweise des Klassennamens erfolgen konnte. Erst zur Laufzeit wurde der dynamische Typ nicht bzw. falsch erkannt, weil ein Schreibfehler vorlag. Gerade bei einer so mechanischen Tätigkeit wie dem Übernehmen des Klassennamens in eine Klassen-Variable treten Schreibfehler jedoch verstärkt auf.

Um diesen Problemen zu begegnen, mußte also eine Lösung gefunden werden, die folgenden Ansprüchen genügte:

·      möglichst einfache Anwendung durch den Programmierer

·      Prüfung der Schreibweise von Klassennamen zur Compilezeit

·      Warnung zur Compilezeit, wenn das Einfügen des RTTI-Mechanismus vergessen wird

·      es braucht nur der dynamische Typ ermittelt zu werden, nicht seine Stellung in der Vererbungshierarchie (andere Abfragen werden nicht benötigt)

·      kein zu großer Implementierungs-Aufwand, da in Kürze die meisten Compiler RTTI unterstützen werden

·      leichter Übergang auf den RTTI-Mechanismus des C++ Standard in späteren Versionen des Frameworks

Zur Implementierung eines Meta-Object-Protocol in C++ gibt es grundsätzlich drei Möglichkeiten (vgl. [Siberski 95]):

1.    Integration in die Sprache bzw. in den Compiler

2.    Verwendung eines Precompilers

3.    Verwendung von Makros

Die erste Variante wird in Zukunft sicher vorherrschen, ist aber ohne den Zugang zum Quell­text des Compilers unmöglich und selbst dann nur mit extrem hohen Aufwand zu bewerk­stelligen. Auch die Verwendung eines Precompilers ist mit einem hohen Erstellungsaufwand für den Precompiler verbunden.

Daher wird meist eine Lösung durch Makros vorgezogen (z.B. in ET++ oder in den Microsoft Foundation Classes). Die Verwendung von Makros arbeitet meist so, daß je ein Makro in die Klassen-Deklaration und die Klassen-Implementation eingefügt wird (vgl. [Schmidberger 95], [Gamma 92, S. 22f]).

In Übereinstimmung mit unseren Design-Grundsätzen für zu erwartende Spracheigenschaften (vgl. Kapitel 5.2.1) haben wir ein ähnliches Verfahren gewählt, das jedoch mit nur einem Makro in der Klassen-Deklaration auskommt:

·      In jede Klassen-Deklaration, die Typinformationen enthalten soll, wird das Makro RTTI(Klassenname) eingefügt.

·      In die Oberklassen sollte statt des Makros RTTI(Klassenname) das Makro RTTIBASE(Klassenname) eingefügt werden. Dies ermöglicht eine bessere Prüfung, ob eine Unterklasse den RTTI-Makro vergißt.

·      Zur Laufzeit kann der dynamische Typ eines Objekts mit dem Makro TYPEID(Objekt) abgefragt und mit dem Ergebnis von CLASSID(Klassenname) verglichen werden.

Diese Realisierung der Typabfrage ähnelt stark dem kommenden C++ Standard. Die folgende Implementierung erfüllt die erwähnten Ansprüche. Für die Implementierung der Makros werden bereits jetzt zwei Implementierungen zur Verfügung gestellt: Eine für Compiler mit RTTI-Unterstützung und eine für Compiler ohne RTTI-Unterstützung.

 

#define RTTIBASE(ClassName)  virtual String oGetType(void) = 0;

#define RTTI(ClassName)      static void __foo(void) {}; \

virtual String __oGetType(void) const \

{ static String __oType = #ClassName; \

ClassName::__foo(); return __oType; };

#define TYPEID(obj)          (obj).__oGetType()

#define CLASSID(type)        (type::__foo(), #type)

Abbildung 28: RTTI-Makros für Compiler ohne RTTI

Das Makro RTTIBASE fügt in die Klasse eine abstrakte Methode oGetType ein, die eine String-Repräsentation der Klasse (bei uns den Klassennamen) liefert. Alle Unterklassen müssen das Makro RTTI verwenden, das eine Implementation für oGetType in die Klasse einfügt (siehe Abbildung 29). Da Makros einen reinen Textersetzungsmechanismus verwenden, kann zur Compilezeit weder geprüft werden, ob der Parameter des RTTI-Makros der Name der jeweiligen Klasse ist bzw. ob es sich überhaupt um einen Klassennamen handelt. Um sicherzustellen, daß es sich bei dem Parameter zumindest um einen in diesem Kontext bekannten Klassennamen handelt, erzeugt das Makro (ansonsten unnötigen) Code, der auf den Parameter als Klasse zugreift. So wird zur Compilezeit ein Syntaxfehler provoziert, wenn der Parameter z.B. durch einen Tippfehler nicht stimmt.

 

class ocAnwendungsklasse {

public:

    RTTI(ocAnwendungsklasse);      // einzige einzufügende Zeile

 

    virtual void vMethode(void);

    // ...

};

Abbildung 29: Einfügen der RTTI-Informationen in Anwendungsklassen

Um von der internen Realisierung zu abstrahieren, werden die Makros TYPEID verwendet, um den dynamischen Typ von Objekten zu erfragen. Da der Inhalt der String-Repräsentation einer Klasse nicht bekannt ist (es wird lediglich garantiert, daß er für alle Objekte der Klasse gleich ist und sich von allen anderen Klassen unterscheidet), kann die String-Repräsentation einer Klasse mit dem Makro CLASSID aus dem Klassennamen erzeugt werden.

 

if ( TYPEID(oObject) == CLASSID(ocClass) )

{

      // der dynamische Typ entspricht der Klasse ocClass

}

else {

      // das Objekt hat einen anderen dynamischen Typ

}

Abbildung 30: Verwendung der RTTI-Informationen zur Laufzeit

Es ist zu erwarten, daß der C++-Standard demnächst verabschiedet wird und in Zukunft alle Compiler den RTTI-Mechanismus beherrschen werden. Aus Effizienzgründen wird es dann sinnvoll sein diesen zu verwenden und nicht mehr mit eigenen String-Repräsentationen zu arbeiten. Um dann eine erneute Umstellung zu vermeiden ist bereits jetzt eine zweite Version des Makros erstellt worden, die den Mechanismus des Compilers verwendet (siehe Abbildung 31). Sobald das Framework mit einem Compiler mit RTTI-Unterstützung eingesetzt wird, wird zunächst nur die Implementierung der Makros ausgetauscht. Die Anwendungen sind hiervon völlig unberührt. (Bei einem Compilerwechsel müssen sie ohnehin neu compiliert werden.)

 

#define RTTIBASE(ClassName)        /* */

#define RTTI(ClassName)            /* */

 

#define TYPEID(obj)                typeid(obj)

#define CLASSID(type)              typeid(type)

Abbildung 31: RTTI-Makros für Compiler mit RTTI

Bei einem größeren Versionswechsel wäre es dann irgendwann in der Zukunft denkbar, völlig auf die Makros zu verzichten und direkt den RTTI-Mechanismus des Compilers zu verwenden.

Die hier vorgestellte Implementation ist einfacher einzusetzen als andere makro-basierte Implementierungen, da sie sich auf die im Kontext des Telefonie-Frameworks benötigte Funktionalität beschränkt. Gleichzeitig wird sie die Evolution des Frameworks durch die Orientierung am kommenden C++-Standard erleichtern.

5.2.3      Designrichtlinien für die Verwendung des Meta-Object-Protocol

Man kann in vielen Systemen, die auf eine lose Kopplung achten, beobachten, daß immer wieder Fälle auftreten, wo der Zugriff auf den dynamischen Typ notwendig ist (s.o.). Dennoch bedeutet eine Typumwandlung einen schweren Eingriff in das Typsystem und sollte daher mit Vorsicht eingesetzt werden.

Der unüberlegte Einsatz eines MOP kann sogar die Bemühungen um eine lose Kopplung behindern. Sobald z.B. Klassen unter ihrer Oberklasse übergeben werden, vor ihrer Verwendung aber immer eine Typumwandlung auf den dynamischen Typ erfolgt, liegen oft ebenso viele Annahmen über den konkreten Typ vor wie bei einer direkten Übergabe.

Man muß sich immer die Frage stellen, ob der Bedarf an Meta-Informationen nicht das Resultat eines schlechten Designs ist und der gleiche Effekt auch eleganter durch die Redefinition von Methoden der Oberklasse gelöst werden kann, die polymorph aufgerufen werden können.

Für den Einsatz des Meta-Object-Protocols für die Übergabe von Objekten zwischen zwei lose zu koppelnden Bereichen haben wir uns daher folgende Designrichtlinien zu eigen gemacht:

·      So weit wie möglich sollte die Kommunikation zwischen unabhängigen Bereichen über abstrakte Klassen erfolgen.

·      Die abstrakten Oberklassen sollten ausschließlich fachlich motiviert sein.

·      Der Zugriff auf die konkrete Klasse mittels MOP darf nur für zurückgegebene Objekte verwendet werden, anderenfalls besteht keine lose Kopplung, wenn in beiden Bereichen immer der konkrete Typ bekannt sein muß.

Insbesondere die dritte Richtlinie ist wichtig: Sobald der dynamische Typ benötigt wird, ist dies ein Zeichen dafür, daß Annahmen gemacht werden, die über die Garantien der Oberklasse hinaus gehen! In einigen wenigen Fällen mag dies gerechtfertigt sein. Die Implikationen für die Kopplung dürfen dabei jedoch nicht übersehen werden.

Insbesondere besteht die Gefahr, daß im Laufe der Evolution eines Frameworks durch "kleine Erweiterungen hier und da", die durch den Zugriff auf den dynamischen Typ gelöst werden, ursprünglich unabhängige Bereiche plötzlich eng gekoppelt sind, da Annahmen über den konkreten Typ gemacht werden. Diese Degeneration des ursprünglichen Designs ist äußerst schwer zu erkennen, da die Parameterübergabe ja weiterhin ausschließlich unter der Oberklasse erfolgt.

5.3          Evolution der Zustandsmodellierung

Wie in Kapitel 3.4 dargelegt, muß die virtuelle Tk-Anlage ein Modell des aktuellen Zustands der realen Tk-Anlagen haben. Hierzu verwenden wir das in Kapitel 3.4.1 beschriebene Half-Call-Modell.

Über den Zustand der Tk-Anlage hinaus muß auch noch der Abarbeitungszustand der an die Tk-Anlage geschickten Requests verfolgt werden. Da die Requests aus mehreren Schritten bestehen können, ist es wichtig, die erfolgreiche Erledigung der einzelnen Schritte z.B. zum Verbindungsaufbau zu verfolgen, um danach den nächsten Schritt einzuleiten bzw. Erfolg oder Mißerfolg des Requests zu signalisieren.

Der Zustand und die notwendigen Arbeitsschritte werden im Telefoniebereich üblicherweise durch endliche Automaten (engl. finite state machines, FSM) modelliert. Es stellt sich daher die Frage, wie eine solche FSM objektorientiert zu modellieren ist. Hinzu kommt der Wunsch, allgemeine Abläufe spezialisieren zu können (z.B. für eine neue Tk-Anlage), ohne alle Zustände und Zustandsübergänge neu implementieren zu müssen.

Bei Micrologica gibt es bereits seit langem eine Bibliothek mit C-Funktionen, die die Modellierung von FSMs unterstützen. Darauf basierend wurden für die High-Level Telefonie-Prozesse des MCC einige C++-Klassen entwickelt, die FSMs kapseln sollen und auch eine Vererbung ermöglichen [Thießen 96].

Abbildung 32: Zustandsmodellierung mit Micrologica-FSM-Klassen

Diese Klassen wurden zunächst zur Modellierung der Requests verwendet. Beim Einsatz dieser FSM-Klassen traten jedoch einige Probleme auf:

·      Die Zustände können nicht benannt werden, sondern müssen fortlaufend auf­steigend durchnumeriert sein. (Es können nur die Aktionen beim Erreichen und Verlassen des Zustands benannt werden.)

·      Um eine FSM in diesem Modell implementieren zu können, müssen vorher der Automat aufgezeichnet und die Zustände numeriert werden.

·      Die beim Erreichen bzw. Verlassen eines Zustands auszuführenden Methoden werden als Methoden-Zeiger übergeben. Es findet keine Überprüfung statt, ob alle Methoden vom selben Objekt sind.

Aus der Arbeit mit diesen FSM-Klassen und den daraus resultierenden Problemen haben wir folgende Forderungen an eine benutzer­freundlichere und leichter änderbare FSM-Implementierung aufgestellt:

·      Die Zustände sollten symbolisch benannt werden können.

·      Die FSM sollte auch im nachhinein leicht erweiterbar sein.

·      Die Spezialisierung von Zuständen sollte einfach möglich sein, da sie sehr häufig benötigt wird.

·      Die Spezialisierung von Übergängen soll möglich sein, wird aber wahrscheinlich selten benötigt.

In der Literatur werden diverse Möglichkeiten diskutiert, wie FSMs objektorientiert modelliert werden können: State-Muster (vgl. [GHJV 95, S. 305]), tabellenorientierte Ansätze (siehe z.B. [Stevenson 95], [Ackroyd 95]) oder eine Erweiterung der sog. "reification technique (RT)" (siehe [SaneCamp 95]).

Wir haben uns für eine Umstellung auf das State-Muster entschieden, da die Mächtigkeit für uns ausreichend ist und jeder Zustand als eine Klasse modelliert ist, die später durch Vererbung auch spezialisiert werden kann. Durch diese Zustandsklassen wird ein benannter Kontext für die in dem jeweiligen Zustand auszuführenden Aktionen gebildet, was bei der Modellierung sehr hilfreich war und auch das "Hineindenken" in diesen Zustand bei späteren Änderungen erleichtert hat.

Abbildung 33: Zustandsmodellierung mit State-Muster

Bei der Spezialisierung der Zustände trat das Problem auf, daß die Zustände global als Singleton-Objekte vorliegen. Da diese über den Klassennamen referenziert und dann als Singleton-Objekt von dieser Klasse erzeugt werden, bestand keine Möglichkeit, einen referenzierten Zustand durch Vererbung zu spezialisieren.

Der erste Lösungsansatz bestand darin, eine Singleton-Registry einzuführen, wie in [GHJV 95, S. 130ff] vorgeschlagen. Um einen Zustand zu spezialisieren, meldete sich ein neuer Zustand unter dem Namen des ursprünglichen Zustands bei der Singleton-Registry an und überschrieb so den alten Zustand. Diese Lösung hatte jedoch eine Reihe gravierender Nachteile:

·      die Spezialisierung der Zustände ist global, d.h. falls ein Zustand aus verschiedenen Kontexten heraus erreicht wird, wird immer der spezialisierte Zustand erreicht, was gelegentlich zu Verwirrung führte

·      die so entstehenden Automaten sind kaum mehr verständlich, da das Überschreiben der Zustände erst zur Laufzeit stattfindet und man leicht den Überblick verliert, welcher Zustand tatsächlich noch in Benutzung ist und welcher überschrieben wird und daher nie erreicht werden kann

·      die Realisierung der endlichen Automaten wird vermischt mit dem Singleton-Mechanismus, der auch an anderen Stellen benötigt wird

Die Probleme der Unübersichtlichkeit scheinen im Ansatz der Spezialisierung von Automaten zu liegen, die prinzipiell problematisch ist. Überhaupt ist die Spezialisierung, Vererbung oder auch Einbettung von endlichen Automaten ein noch keineswegs intensiv betrachtetes Gebiet und birgt eine sehr hohe Komplexität. Eine vertiefende Betrachtung der Probleme und Lösungs­ansätze ist in [SaneCamp 95] zu finden. Das dort vorgestellte Verfahren und die dazu notwendige Modellierung sind jedoch recht aufwendig.

Wir haben uns aufgrund der hohen Komplexität dieses Themas auf die von uns unbedingt benötigte Funktionalität beschränkt und keinen allgemeinen Ansatz zur Spezialisierung von endlichen Automaten gesucht. Dabei sind wir zu folgendem Modell gekommen:

·      Automaten können nur als Ganzes spezialisiert werden

·      Zustände werden als Singleton-Objekte repräsentiert, die über ihren Klassennamen referenziert werden, aber es ist keine Spezialisierung des Zustands möglich, d.h. es wird immer auch tatsächlich der über den Namen referenzierte Zustand erreicht

·      um einen Automaten zu spezialisieren, wird sein Start-Zustand spezialisiert

·      der spezialisierte Zustand kann Zustandsübergänge in andere spezialisierte Zustände haben, oder in die Zustände des Original-Automaten. Sobald ein Zustand des Original-Automaten erreicht ist, verhält sich der Automat wieder wie der ursprüngliche Automat (spezialisierte Zustände können dann nicht mehr erreicht werden)

·      jeder spezialisierte Zustand ruft immer die Methoden seines Ober-Zustands auf

Dieses Modell hat sich für die von uns benötigten Automaten als praktikabel herausgestellt, da üblicherweise nur sehr wenige Zustände nach dem Start-Zustand spezialisiert werden mußten. Danach konnten meist die ursprünglichen Zustände weiterverwendet werden. Diese Form der Anpaßbarkeit hat genügt, um die Automaten hinreichend leicht verändern zu können, und hat damit ausgereicht, um die Hauptprobleme der Micrologica-FSM-Klassen zu beheben.

Eine weitere Erfahrung mit der Implementierung von Automaten mit dem State-Muster betrifft die enorme Anzahl der benötigten Klassen. Um 10 Request-Typen für 1 Tk-Anlage zu implementieren, wurden ca. 60 Zustands-Klassen benötigt. Für die Spezialisierung der Request-Typen je weiterer Tk-Anlage würden noch einmal ca. 20 zusätzliche Klassen benötigt. Alle diese Zustands-Klassen sind jedoch sehr einfach, so daß der Overhead der Klassen­deklaration im Vergleich zum Umfang der Methoden beachtlich groß ist. Mit den Micrologica-FSM Klassen wären bei einer vergleichbaren Implementierung nur ca. 20-30 Klassen nötig gewesen. Die Frage, wie man endliche Automaten in objektorientierten Systemen implementieren kann, scheint daher noch nicht befriedigend gelöst.

Die Problematik der Spezialisierung von Automaten hat sich als deutlich komplexer heraus­gestellt, als zunächst angenommen. Die gefundene Lösung ist weit davon entfernt, allgemein­gültig zu sein, aber sie hat das konkrete Problem gelöst. Der Umgang mit derartigen Automaten ist mit Sicherheit ein interessantes Forschungsthema, falls es gelingt, die Komplexität im Umgang mit den Automaten in den Griff zu bekommen.

5.3.1      Aufzählungstypen

Eine andere, häufig verwendete Technik, um eine Menge von Zuständen zu repräsentieren, ist die Definition eines Aufzählungstyps, der alle möglichen Zustände enthält. Für die Zustands­modellierung des Half-Call-Modells wurde zunächst ihre Verwendung erwogen, dann aber aus Gründen der mangelhaften Erweiterbarkeit verworfen.

Die Modellierung von Zuständen durch Aufzählungstypen macht den möglichen Wertebereich deutlich und erleichtert damit die Lesbarkeit des Quelltextes. Dennoch hat sie einen gravierenden Nachteil: der Wertebereich ist im nachhinein kaum änderbar. Das bedeutet, daß eine Evolution eines Frame­works, das Aufzählungstypen verwendet, nur durch die Modifikation des Framework-Quell­textes möglich ist - nicht aber durch Spezialisierung.

Die Ursache liegt darin, daß Aufzählungstypen in C++ keine Klassen sind und daher im nach­hinein nicht mehr erweitert werden können, ohne den ursprünglichen Quell­text zu verändern. Sie verletzen damit das Offen-Geschlossen-Prinzip (engl. open-closed principle) [Meyer 88].

Definition 24: Offen-Geschlossen-Prinzip

Das Offen-Geschlossen-Prinzip besagt, daß eine wiederverwendbare Komponente sowohl offen für Erweiterungen sein soll als auch abgeschlossen, d.h. in einem stabilen Zustand, so daß sie (für den Anwendungsprogrammierer unerreichbar) z.B. in einer Bibliothek abgelegt werden kann.

Da ein Framework oft in compilierter Form vorliegt, ist eine Änderung der Definition eines Aufzählungstyps durch den Anwendungsprogrammierer nicht möglich. Die Anforderungen verschiedener Anwendungsprogrammierer können auch durchaus gegenläufig sein, so daß eine zentrale Änderung im Framework oft nicht sinnvoll ist.

Als Ersatz für Aufzählungstypen sind in C++ Defines und Konstanten möglich. Bei beiden können im nachhinein Werte hinzugefügt werden, sie haben jedoch den Nachteil, daß keine Über­prüfung stattfindet, ob der betreffende Wert nicht an anderer Stelle bereits mit anderer Bedeutung verwendet wird.

In Fällen, wo nur eine geringe Zahl von Zuständen benötigt wird, bietet sich das Ausweichen auf eigene Klassen für die Zustände an. Diese können dann polymorph (siehe z.B. State-Muster [GHJV 95, S. 305ff]) oder im Zusammenhang mit einem Meta-Object-Protocol eingesetzt werden.

5.3.2      Eine wiederverwendbare Singleton-Implementation

Da die Zustandsmodellierung bei den Requests in großem Stil das State-Muster einsetzt, standen wir vor dem Problem, Vererbungshierarchien von State-Objekten zu realisieren. Diese State-Objekte sind zustandslos, da sie nur das Verhalten in einem Zustand repräsentieren, und werden daher als Singletons modelliert. Bei der Realisierung anhand der Beispiel-Implementation des Singleton-Musters aus [GHJV, S. 128ff] wurde deutlich, daß diese Implementation nicht wiederverwendbar ist und gravierende Einschränkungen aufweist.

Ich werde zunächst auf die Defizite der Beispiel-Implementation eingehen und danach auf­zeigen, wie man durch generative Programmierung (vgl. [Eisenecker 96]) zu einer wieder­verwend­baren Implementation kommen kann. Über die folgende Singleton-Implementierung hinaus lassen sich sicher noch diverse andere Implementationsprobleme mit generativer Programmierung sehr elegant lösen.

Das Singleton-Muster soll ausdrücken, daß es von einer bestimmten Klasse im laufenden System nur maximal ein Objekt geben kann bzw. soll. Die übliche Implementation in C++ (vgl. [GHJV 95], S. 128ff) verwendet eine static-Variable in der Singleton-Klasse, die einen Zeiger auf das einzige erzeugbare Exemplar hält. Wenn dieser Zeiger noch nicht gesetzt ist, wird ein neues Exemplar erzeugt, sonst wird immer ein Zeiger auf das bereits erzeugte Exemplar zurückgeliefert.

Wenn man in einer Anwendung mehrere Klassen zu Singletons machen möchte und z.B. Vererbungs­hierarchien von Singletons hat, treten gleich eine ganze Reihe von Problemen auf:

·      Unterklassen einer Singleton-Klasse erben die Singleton-Eigenschaft nicht. Die Initialisierung der static-Variable muß immer in der konkreten Klasse geschehen, da in C++ static-Methoden nicht dynamisch gebunden werden können. Die Unterklasse muß die Initialisierung daher explizit selbst vornehmen. Daher wird in [GHJV 95, S. 130ff] für solche Fälle mit Vererbung eine Implementierung mit einer globalen Registry vor­geschlagen. Da sich alle Objekte dort registrieren müssen, muß es von jeder Klasse ein static-Objekt geben.

·      Da alle Klassen static instanziiert werden müssen, dürfen die Konstruktoren nicht protected sein. In der ursprünglichen Implementation konnte so verhindert werden, daß auf andere Weise als vorgesehen Objekte der Klasse erzeugt werden. Bei einer Registry-Implementierung ist dies nicht möglich, und die Singleton-Eigenschaft kann nicht mehr vom Compiler überprüft werden!

·      Die Instanziierung des static-Klassenattributs darf nur in einer Übersetzungseinheit vorkommen (d.h. darf nicht in der Header-Datei erfolgen, sondern muß in einer Implementations-Datei geschehen), damit es nicht doppelt definiert ist. Diese Distanz im Quell­text erhöht die Gefahr, die Instanziierung zu vergessen. Wenn die static-Instanziierung der Klasse vergessen wird, führt dies jedoch erst zur Laufzeit zu Fehlern.

Von diesen Implementierungs-Problemen inspiriert, stelle ich einige Forderungen an eine Singleton-Implementierung in C++ auf:

·      Der Konstruktor einer Singleton-Klasse sollte protected (bzw. private) sein, um irrtümliche Instanziierungen zu verhindern.

·      Man sollte von Singleton-Klassen erben und diese Unterklassen ebenfalls zu Singletons machen können.

·      Der Anwendungsprogrammierer sollte sich um möglichst wenige technischen Details der Singleton-Implementierung (z.B. static-Instanziierungen) kümmern müssen.

In unserem Framework werden z.B. für Events und in der Zustandsmodellierung größere Singleton-Hierarchien verwendet. Wir setzen dafür eine modifizierte Version des von Douglas C. Schmidt in [Vlissides 96b] vorgeschlagenen Singleton-Template ein, bei der im Gegensatz zum Vorschlag von Vlissides nicht die zum Singleton zu machende Klasse vom Singleton-Template erbt, sondern umgekehrt.

 

template <class TYPE>

class ocSingleton : public TYPE

{

protected:

    ocSingleton<TYPE>() { };

    ocSingleton<TYPE>(const ocSingleton<TYPE> &) { };

 

public:

    static ocSingleton<TYPE> * poInstance(void);

};

 

template <class TYPE>

ocSingleton<TYPE> * ocSingleton<TYPE>::poInstance(void)

{

    static ocSingleton<TYPE> * _poinstance = 0;

    if (_poinstance == 0) {

        _poinstance = new ocSingleton<TYPE>;

    }

    return _poinstance;

}

Abbildung 34: Singleton-Implementierung als Template

Diese Implementierung erfüllt die drei postulierten Forderungen und erzeugt den konkreten Typ erst zur Compilezeit aus dem Template und der Anwendungsklasse. Dadurch können sich sowohl die Template-Implementation als auch die Anwendungsklasse getrennt voneinander entwickeln.

Der Anwendungsprogrammierer stellt lediglich sicher, daß der Konstruktor und Copy-Konstruktor seiner Klasse protected ist. (Der Zuweisungsoperator braucht nicht geschützt zu werden, da ohnehin kein Speicher für Objekte seiner Klasse alloziert werden kann.)

Dadurch entsteht folgende Vererbungsstruktur:

Abbildung 35: Vererbungshierarchie mit Singleton-Template

Da bei dieser Implementierung die Konstruktoren wieder geschützt sein können, können die Klassen selbst nicht irrtümlich verwendet werden, sondern nur in Verbindung mit dem Singleton-Template.

Da die per Template erzeugten Singletons nun aber nicht mehr Unterklassen voneinander sind, dürfen nur Variablen der Anwendungsklasse deklariert werden, wenn von Polymorphie Gebrauch gemacht werden soll. Dies entspricht aber auch der Anwendungssemantik. Das Template tritt nur beim Zugriff auf Singleton-Exemplare in Erscheinung.

Zusätzlich verwendet unser Singleton-Template eine static-Variable innerhalb der Erzeugungs­methode anstatt in der Klasse. Daher wird die bei allen [GHJV 95] Implementationen not­wendige Initialisierung der static-Variable dem Programmierer abgenommen und kann im Template geschehen, da in C++ nur static-Variablen von Methoden, nicht aber von Klassen direkt bei ihrer Deklaration initialisiert werden können.

Ein wesentlicher Vorteil bei einer Template-Implementierung ist, daß gemäß dem Prinzip der "Separation of Concern" die Schnittstelle der Anwendungsklasse und ihre Implementierung getrennt sind von der Singleton-Implementierung. Auf diese Weise wird die Wieder­verwendbarkeit und die Evolutionsfähigkeit erhöht.

5.4          Entwicklung der Event-Normalisierung

Die im Anwendungsbereich begründete Notwendigkeit, eine Event-Normalisierung (siehe Definition 21) durchzuführen, wurde bereits früh erkannt. Wie in Kapitel 3.5.4 beschrieben, reicht die Spezifikation der gängigen Telefonie-Protokolle nicht aus, um Tk-Anlagen-unabhängige Telefonie-Anwendungen zu entwickeln.

Um diesem Problem zu begegnen, verwendet das Telefonie-Framework eine sehr viel detailliertere Protokollbeschreibung. Bei der Entwicklung dieser Protokollbeschreibung wurde einerseits darauf geachtet, daß sie nicht im Widerspruch zu den üblichen Standards steht, andererseits jedoch in den für das MCC wichtigen Bereichen ausreicht, um den Anwendungs­entwicklern genügend Zusicherungen über das Verhalten der Tk-Anlage zu geben, so daß sie völlig anlagenunabhängig entwickeln können.

Somit verhalten sich die Micrologica-TSERV-Prozesse völlig standard-konform, so daß Programme von Fremdanbietern verwendet werden können, und andererseits bieten sie soviel Zusicherungen, wie sonst oft nur mit einem proprietären Protokoll zu erreichen sind.

Um dieses Verhalten zu realisieren, ist es notwendig, in der virtuellen Tk-Anlage ein sehr detailliertes Modell des Tk-Anlagen-Zustands zu halten, so daß zusätzliche Events generiert und überflüssige Events verschluckt werden können. Die Interaktionsdiagramme in Abbildung 36 zeigen, wie unterschiedliche Eventfolgen verschiedener Tk-Anlagen normalisiert werden, so daß der Anwendung ein fast identischer Event-Strom zur Verfügung gestellt werden kann. Lediglich das Timing der Events ist leicht abweichend.

 

Tk-Anlage A

Tk-Anlage B

Abbildung 36: Event-Normalisierung durch den Tk-Anlagen-Treiber

In der ersten Version des Frameworks wurden die Events der Tk-Anlage dem jeweiligen Request zugeordnet, das sie ausgelöst hat. Dieses Request bzw. die darin enthaltene FSM bot den nötigen Kontext, um fehlende Events zu ergänzen und die richtige Reihenfolge herzu­stellen. Abbildung 37 zeigt den (vereinfachten) Ablauf einer Event-Normalisierung, bei der aus einem Event A einer konkreten Tk-Anlage zwei Events generiert werden.

Dabei wird folgender Ablauf verwendet (Formatkonvertierungen werden zur Vereinfachung außer acht gelassen):

Ein Event von der Tk-Anlage wird vom Event-Konverter an die virtuelle Tk-Anlage übergeben. Diese ordnet das Event dem Request-Objekt zu, das das Event ausgelöst hat. Entsprechend dem State-Muster übergibt das Request-Objekt das Event an seinen aktuellen Zustand. Anhand des aktuellen Zustands werden dann mehrere Events generiert. Die normalisierten Events werden dann vom Telefonie-Protokoll-Konverter an die Anwendungen gemeldet.

 

Abbildung 37: Erste Version der Event-Normalisierung

Erst einige Zeit nachdem dieser Ablauf implementiert und auch getestet war, fiel ein Denk­fehler auf: Auf diese Weise lassen sich nur dann alle Events normalisieren, wenn alle Aktionen der Tk-Anlage durch Requests veranlaßt wurden. Bei den durchgeführten Tests ist dies auch stets der Fall gewesen, aber das Framework sollte auch in einer LAN-zentrierten Konfiguration eingesetzt werden, bei der keineswegs eine exklusive Steuerung der Tk-Anlage gewährleistet ist (vgl. Kapitel 3.5.2). Daher kann nicht davon ausgegangen werden, daß alle eingehenden Events von Requests des Telefonie-Frameworks ausgelöst wurden; sie können auch von anderen Benutzern der Tk-Anlage verursacht worden sein.

Um dieses Problem zu lösen, ist es notwendig, ein vollständiges Modell aller Aktivitäten der Tk-Anlage im Tk-Anlagen-Treiber vorzuhalten - unabhängig davon, wer sie mit welcher Intention ausgelöst hat. Daher wurde eine zusätzliche Instanz, der sog. Call-Manager, eingefügt, die den aktuellen Zustand der Tk-Anlage in Half-Call-Modellierung (siehe Kapitel 3.4.1) vorhält (sofern sie ihr über Events bekannt werden).

Die Normalisierung der Events wird nun von einem sog. Event-Normalisierer durchgeführt, der auf das Modell des Call-Managers zurückgreift und daraus die zu erzeugenden Events ableitet. Diese Art der Event-Normalisierung ist deutlich komplexer, insbesondere deshalb, weil die Intention nicht bekannt ist, warum bestimmte Verbindungen aufgebaut werden. Vorher war durch den Typ des Requests bekannt, was erreicht werden sollte. Die folgende Abbildung zeigt den neuen Ablauf:

 

Abbildung 38: Zweite Version der Event-Normalisierung

Bei der Implementation des neuen Ablaufs hat es sich als sehr positiv erwiesen, daß der Bereich der virtuellen Tk-Anlage hinter einer Fassade verborgen war. Auf diese Weise sind die anderen Bereiche von dieser unerwarteten Änderung völlig unberührt geblieben. Es mußte lediglich der interne Ablauf in der virtuellen Tk-Anlage geändert werden.

Im nachhinein betrachtet, könnte man natürlich argumentieren, daß man die Unzulänglichkeit der ersten Implementation vorher hätte erkennen können und sollen. Andererseits ist diese Änderung das Ergebnis des Lernprozesses der an der Entwicklung Beteiligten, und man wird nie alle Probleme bei der ersten Analyse erkennen können. Daher macht dieses Beispiel deutlich, wie wichtig ein flexibles Design ist, wenn es darum geht, mit späteren Änderungen umzugehen.

Es ist bei der Framework-Entwicklung durchaus üblich, daß neue, wichtige Erfahrungen in dem jeweiligen Anwendungsbereich gemacht werden, da sie oftmals Anlaß für eine unabhängige, neutrale Betrachtung des Anwendungsgebietes bietet.

Obwohl sie in der Literatur kaum anzutreffen ist, möchte ich die These aufstellen, daß die Event-Normalisierung eine allgemein verwendbare Technik zur Hardware-Steuerung ist. Eine ähnliche Technik wird offenbar auch im "CallPath" Telefonie-System von IBM eingesetzt (siehe [Salamone 96]), über deren Implementation mir leider keine Publikationen bekannt sind. Es wäre ein lohnenswertes Unterfangen zu untersuchen, ob sich diese Technik weiter verallgemeinern läßt.


5.5          Evolution der Kern-Abstraktion

Die folgenden Abbildungen sollen die gedankliche Entwicklung der ersten Framework-Version verdeutlichen: Sie sind in dieser Form nicht implementiert worden und stellen nur die Evolution des Designs dar und nicht die Evolution einer Implementierung.

Der Grundgedanke, der bei Micrologica schon lange existierte, war, daß man den TSERV-Prozeß als Protokoll-Konverter betrachten müßte. Er konvertiert ein Protokoll A in ein Protokoll B, genauer: Er konvertiert ein Telefonie-Protokoll in ein Tk-Anlagen-Protokoll.

 

1. Stufe

2. Stufe

3. Stufe

4. Stufe

 

Vermischung der Protokolle

Trennung der Protokolle

Austauschbare Protokoll-Komponenten

Protokoll-Komponenten als Adapter auf gemeinsamem Kern

Abbildung 39: Die ersten Entwicklungsstufen des Designs

Bei Micrologica waren daher Darstellungen in der Art von Stufe 2 (Abbildung 39) gebräuchlich, obwohl sich die vorhandene Implementation eher auf Stufe 1 befand. Die bis­herigen TSERV-Implementationen haben beide Protokolle untrennbar miteinander verbunden. Sie sind jedoch bereits mit dem Gedanken der Protokoll-Konvertierung entwickelt worden.

Es hat sich für interne Diskussionen als sehr hilfreich erwiesen, auf diese Darstellung zurück­zugreifen und die Design-Idee des neuen Frameworks graphisch als Evolutionsschritte aus der ursprünglichen Darstellung abzuleiten. Dieses Vorgehen vermittelte das Gefühl von Kontinuität und hob den Wert des existierenden fachlichen Konzepts hervor.

 

Erste Implementierung

Aktuelles Design

 

Trennung von Event- und Request-Konvertierung

FSM-Steuerung der Tk-Anlage

Abbildung 40: Implementierte Versionen

Bei der Implementierung stellte sich sehr schnell heraus, daß die Protokoll-Konverter zwei weitgehend getrennt Funktionen erfüllen: die Konvertierung von Events und die Konvertierung von Requests. Diese Teile sind dann auch getrennt implementiert worden.

Bei der Implementierung der Tk-Anlagen-Steuerung wurde jedoch festgestellt, daß diese nicht durch einen simplen Konverter zu bewerkstelligen ist, da sie in mehreren Tk-Anlagen-spezifischen Schritten abläuft, die Zugriff auf das interne Modell der Tk-Anlage benötigen (siehe Kapitel 3.4.1). Um diese Schritte einzeln spezialisieren zu können, wurde daher die Ablauf­steuerung im nächsten Evolutionsschritt mit Zustands­maschinen modelliert. Die Trennung dieser Zustandsmaschinen in allgemeine und Tk-Anlagen-spezifische Teile ist bereits bei der Erklärung der Requests in Kapitel 4.3.2 dargestellt worden.

5.6          Zusammenfassung

Die in diesem Kapitel vorgestellten Entwicklungsschritte haben zu dem Konzept von lose gekoppelten Framework-Bereichen geführt, das in Kapitel 6 noch ausführlich diskutiert wird. Es hat sich gezeigt, daß die explizite Bildung von "Framework-Bereichen" dazu beigetragen hat, daß die Auswirkungen von Änderungen weitgehend gekapselt werden konnten.

Andererseits ist die Evolution des Telefonie-Frameworks natürlich auch ein Resultat des Einsatz­kontextes. Der intensive Kontakt zu den Telefonie-Spezialisten hat sich ausgezahlt. Es sind wenige gravierende Änderungen aufgetreten, die die Grundannahmen, auf denen das Framework basiert, ins Wanken hätten bringen können.

 


Last updated: 24. Aug 2005
Page maintained by Jan Willamowius
Impressum · Datenschutz
 
English: Home | Linux | Perl | Java | Eiffel | Books | Music | Jan Willamowius | Updates | Site Map
Deutsch: Home | Badminton | ISBN-Suche | Musik-Suche | Rezepte | Jan Willamowius