Session D-TIER

Three-Tier-Development

Manfred Rätzmann
RÄTZMANN GmbH


Einführung

Worum geht’s?

Die 3-Tier (oder 3 Schichten) Architektur ist die zur Zeit angesagte Software-Architektur. Warum? Nun, auf der einen Seite taucht immer häufiger die Forderung auf, daß eine Applikation auch über das Intra- oder Internet mit Hilfe eines Standardbrowsers zu bedienen sein soll, auf der anderen Seite soll zur Speicherung der Applikationsdaten eine bereits vorhandene Serverdatenbank benutzt werden. Dazwischen befindet sich dann die Applikationslogik, die von einem Austausch der Anwendungsoberfläche oder der zu Grunde liegenden Datenbank möglichst unbeeinflußt bleiben soll. Daher hat die Idee, Anwendungsoberfläche, Anwendungslogik und Datenhaltung zu trennen, einen enormen Aufschwung erfahren.

„Prima!“, sagt da der gestandene VFP Entwickler, „Brauch ich das?“ Auch, wenn Ihre Anwendung (noch) nicht Intranet-fähig sein muß und Sie bisher mit der VFP-Datenbankengine bestens zu Rande gekommen sind, lohnt es immer, sich mit dem 3-Tier Architekturprinzip auseinander zu setzen. In dieser Session soll dargestellt werden, wann, warum und wie Sie Ihre VFP-Applikation in 3-Tier Technik aufbauen sollten.

Worum geht’s nicht?

Wir beschäftigen uns hier also weniger mit Intra- oder Internetapplikationen (dazu kann ich zum Beispiel die Sessions von Peter Herzog empfehlen), auch um die Ansteuerung diverser Serverdatenbanken geht es hier nicht (auch dazu gibt es mehrere gute Sessions, z.B.  von Georg Emrich, Nathalie Mengel oder Eldor Gemst). Hier geht’s – um es noch mal zu sagen – um die Verwendung der 3-Tier Architektur in reinen VFP Applikationen.

Warum eigentlich drei Ebenen?

Die Idee, Oberfläche, Anwendung und Datenhaltung zu trennen, ist eigentlich nicht die neueste. Die Anbieter von GUI-Buildern und Server-Datenbanken propagieren sie schon seit längerem. Als allgemeines Architekturprinzip setzt sich diese Idee aber jetzt erst durch. Warum?

  1. Die Objektorientierung hat die Denkweise der Entwickler und Software-Architekten verändert. Statt an sequentiell abzuarbeitende Prozeduren, denken heute viele Leute an miteinander kommunizierende Objekte, wenn sie versuchen, eine Aufgabenstellung in Software „umzuformulieren“. In dieser Denkweise ist die Kommunikation zwischen Oberfläche und Anwendungsschicht sowie zwischen Anwendungsschicht und Datenhaltung eine ganz normale Geschichte. Eine Benutzereingabe bedeutet hier nicht eine notwendige Unterbrechung des abzuarbeitenden Programms, sondern eine der vielen möglichen Kommunikationsvorgänge zwischen zwei Objekten, dem I/O-Objekt und einem Anwendungsobjekt.
  2. Als logische Fortführung des OO-Denkens wurde das komponentenbasierte Design wieder interessant. Auch das ist eigentlich eine schon etwas ältere Idee, die momentan fröhliche Urständ feiert, weil vielleicht erst jetzt die notwendigen Umfeldbedingungen vorliegen.
    „Komponenten sind Objekte, die eine fest umrissene, verallgemeinerte Aufgabe eigenständig lösen, streng gekapselt und für die Kommunikation mit anderen Komponenten vorbereitet sind.“
    Die Idee ist, Programme aus Fertigteilen zusammenzubauen, wie das im Hardwarebereich schon seit langem selbstverständlich ist. Die Wiederverwendbarkeit solcher Fertigteile erhöht sich aber enorm, wenn sie unabhängig von der verwendeten Oberfläche (vom jeweiligen GUI-Builder oder der Programmiersprache, in der die Oberfläche erstellt wird) und unabhängig von der Art der Datenhaltung sind. Die Anwendungsschicht ist also der Bereich der größeren Komponenten, die eine Teilaufgabe der Anwendung, z.B. die Lagerverwaltung, eigenständig lösen.
  3. Die Anwendungsschicht als Sammlung vorgefertigter Komponenten aufzubauen, bietet eine Reihe von Vorteilen:

Die Struktur einer 3-Tier Anwendung

Die 3-Tier Architektur teilt eine Anwendung in Oberfläche, Anwendungsschicht und Datenhaltung. Die Elemente der Oberfläche zeigen Eigenschaftswerte von Anwendungsobjekten an oder rufen Methoden von Anwendungsobjekten auf. Die Anwendungsobjekte arbeiten auf einer logischen Datenbank, also auf anwendungsspezifischen Datenstrukturen, die unabhängig von der physischen Datenhaltung sind. Diese Vorgehensweise wird in VFP durch den View-Support optimal unterstützt. Spezifische Oberflächenelemente wie Grids, Listboxes etc. greifen auch auf Tabellen der logischen Datenbank zu, niemals jedoch auf die physische Datenbank.

Durch das Zwischenschalten einer logischen Datenbank kann sich die physische Speicherung komplett ändern – zum Beispiel können die Daten einer Anwendung auf unterschiedliche physische Datenbanken aufgeteilt werden – ohne daß die Anwendungsschicht oder die Oberfläche angepasst werden müssen.

In dieser Grobstruktur sind Metadaten, die von den Anwendungsobjekten verwendet werden, nicht berücksichtigt. Auf diese Metadaten (Steuerungsparameter, Strukturinformationen u.a.) können die Anwendungsobjekte direkt zugreifen. Solche Metadaten werden im allgemeinen nicht in der logischen Datenbank repräsentiert.

Verantwortlichkeiten der Oberfläche

Die Oberfläche ist für die korrekt formatierte Ein- und Ausgabe der Daten verantwortlich. Dazu benötigte Formatinformationen (z.B. max. Länge von Zeichenketten, Vor- und Nachkommastellen  numerischer Felder, anzuzeigende Währungskürzel etc.) kann sie von den Anwendungsobjekten abfragen. Da moderne Programme ereignisorientiert ausgelegt sein sollen, ist die Oberfläche in weiten Teilen auch für den Programmablauf zuständig. Der Anwender wählt häufig aus Menüs oder durch Klick auf einen Button den nächsten Arbeitsschritt aus.  In allen Fällen, in denen die Programmlogik jedoch einen festgelegten Programmablauf braucht, sollte dieser nicht in Oberflächenelementen sondern in einem darauf spezialisierten Objekt der Anwendungsschicht hinterlegt sein.

Verantwortlichkeiten der Anwendungsschicht

Die Anwendungsschicht ist für alles verantwortlich, was die Anwendungslogik selbst betrifft. Darüber hinaus ist die Anwendungsschicht aber auch für die Konsistenz ihrer Daten verantwortlich. Die Verantwortlichkeit für Feld- und Satzvalidierung und die Überwachung der referentiellen Integrität werden der physischen Datenbank entzogen und der Anwendungsschicht übertragen. Der Grund dafür ist, daß die Anwendungsschicht zum einen völlig unabhängig von der physischen Datenspeicherung sein soll. Sie kann also nicht davon ausgehen, daß die physische Datenbank über Feld- und Satzvalidierungsmöglichkeiten verfügt, bzw. das immer generische Rückgaben von Validierungsfehlern zur Verfügung stehen. Zum anderen können die Daten verschiedener Komponenten der Anwendungsschicht durchaus in getrennten physischen Datenbanken gehalten werden. Damit ist aber die Installation einer RI-Prüfung auf der Ebene der physischen Datenbanken so gut wie ausgeschlossen.

Verantwortlichkeiten der logischen Datenbank

Die logische Datenbank stellt die Verbindung zwischen der Anwendungsschicht und der physischen Datenbank her. Sie muß also gegebenenfalls auf die physische Datenbank zugeschnitten werden. Sie ist verantwortlich dafür, daß der Anwendungsschicht immer eine gleichbleibende Sicht auf die Daten zur Verfügung steht. Dazu gehört auch eine eventuell notwendig werdende Konvertierung von Feldtypen etc. Umgekehrt verteilt sie die aus der Anwendung zu speichernden Daten auf die physischen Tabellen. Hier wird demnach festgelegt, was wo und wie abgespeichert wird.

Verantwortlichkeiten der physischen Datenbank

Die physische Datenbank ist für das schnelle und sichere Speichern und Abrufen der Daten zuständig. Sie stellt dazu Transaktionsmechanismen zur Verfügung, die von der logischen Datenbank benutzt werden können, um zusammenhängende Daten zu speichern.

Ein Modell

Das folgende Modell beschreibt keine konkrete Applikation sondern eine generelle Struktur für 3-Tier VFP Applikationen.

Der linke Teil des Diagramms stellt die Oberfläche dar. In der Mitte befindet sich die Anwendungsschicht, rechts stehen Objekte, die die Verbindung zur logischen Datenbank herstellen.

Object List und DataForm

Die Oberfläche einer 3-Tier Applikation arbeitet im wesentlichen auf zwei Objektarten und zwar auf Objekt-Sets (Collections) und Einzelobjekten. Dem Anwender werden also Objektlisten zur Auswahl des zu bearbeitenden Objekts und DataForms zur Bearbeitung des ausgewählten Objekts und zur Neuanlage von Objekten angeboten. Mit den Navigationsbuttons der Toolbar kann durch eine Objektliste navigiert werden. Die Buttons „Neu“ und „Öffnen“ rufen aus der Objektliste heraus die dazu passende DataForm zum Editieren oder Neuanlegen eines Objektes auf.

Die Elemente auf der DataForm, wie I/O Controls, Listfelder, ComboBoxes sind nicht an Tabellenfelder gebunden, sondern an Eigenschaften von Business-Objekten. Buttons enthalten keinen Verarbeitungscode sondern rufen Methoden des Business-Objekts auf.

ObjectSets und SingleRecord View

Grids arbeiten nur auf Tabellen, List- oder Comoboxes auch auf Arrays, wobei auch hier das Handling von Tabellen einfacher ist. Die Tabellen, auf denen diese Oberflächenelemente arbeiten können, sind Bestandteil der logischen Datenbank. Sie werden durch eine ObjectSet-Klasse repräsentiert. ObjectSets sind MultiRecordViews, sie stellen die Daten selektiv zur Verfügung. Der Anwender kann also die jeweilige Objektliste über Viewparameter eingrenzen. SingleRecordViews stellen immer nur genau einen Satz zur Verfügung. Dieser Satz enthält die Daten eines BusinessObjects.

Business Objects

Die BusinessObject-Klasse ist eine abstrakte Klasse. Sie stellt die Methoden und Eigenschaften zur Verfügung, die von allen konkreten BusinessObject-Klassen benötigt werden.

Object-IDs

Im hier modellierten Beispiel hat jedes BusinessObject eine systemweit eindeutige ID. Diese ID wird durch die Methode _CreateID beim Erschaffen des Objects gebildet und bleibt von dort an unveränderlich. Sie kann also als Referenz auf das BusinessObject verwendet werden. Systemweit eindeutige Object-IDs  ermöglichen auch ein einfaches Object-Locking über Semaphore (siehe unten).

Identität

Instantiierte BusinessObjecte haben keine eindeutige Identität sondern können ihre Identität ändern. Durch Aufruf der Methode New() werden die Eigenschaften des BusinessObjects mit ihren Default-Werten initialisiert. Wenn ein Kunden-Objekt vorher die Identität „Meier“ hatte, hat es diese Identitität nach Aufruf von New() nicht mehr. Ebenso kann mit Load() die Identität gewechselt werden. Die aktuelle Identität eines BusinessObject wird durch die ID festgelegt.

Attribute

Die Attribute des BusinessObjects, deren Werte in Tabellen gespeichert werden müssen, sind Objekte der Klasse DataField. Diese Klasse stellt also die Verbindung zu einem Datenfeld in einer Tabelle her. Die Tabelle selbst ist ein SingleRecordView mit der ID als einzigem Viewparameter. Der View muß updatable sein, damit die Attributwerte über die Save()-Methode gespeichert werden können.

Validierung

Die Validate()-Methode der abstrakten BusinessObject-Klasse ist zunächst leer. Diese Methode kann von einer konkreten BusinessObject-Klasse zu einer Validierung des gesamten Objects vor dem Speichern genutzt werden. Die DataField-Klasse hat ebenfalls eine Validate()-Methode, die zur Validierung des einzelnen Attributs dient. Es gibt also auch auf der Ebene der BusinessObjects sowas wie Feld- und Satzvalidierung in der Datenbank.

Klassen für Datentypen

Direkte Anbindung

Zur Anbindung von Tabellenfeldern der logischen Datenbank an Attribute eines Business-Objects gibt es zwei Möglichkeiten. Zum einen könnte pro Datenfeld der logischenTabelle direkt eine Eigenschaft der Business-Object Klasse angelegt werden. Das Füllen dieser Eigenschaft mit dem jeweiligen Feldwert würde dann von der Load()-Methode des Business-Objects übernommen.

DataField-Klasse

Die zweite Möglichkeit ist, eine DataField Klasse zu verwenden. Das hat einige Vorteile. So können spezifische Ableitungen der DataField-Klasse für unterschiedliche Datentypen gebildet werden. Die DataField Klasse oder daraus abgeleitete Klassen können Eigenschaften und Methoden anbieten, die von den Controls der jeweiligen Oberfläche genutzt werden können. Im obigen Beispieldiagram wären dies die Eigenschaften nMaxLength, nMinLength und lMixedCase der Klasse CharacterData. Ein Control, das auf die Anzeige von CharacterData Objekten spezialisiert ist, kann diese Werte auslesen und die Anzeige entsprechend aufbereiten.

Feldvalidierung

Mit der eigenen Validate()-Methode bietet eine DataField-Klasse die Möglichkeit der Feldvalidierung vor dem Speichern. Auch die Validate()-Methode kann auf die Eigenschaften des DataField Objects zugreifen und somit sehr gut allgemeingültig gehalten werden.

Neue Attributtypen

Ein weiterer Vorteil der DataField Klasse ist, daß aus ihr auch Klassen wie EnumerationData abgeleitet werden können. Für die Attribute des BusinessObjects stehen damit auch Datentypen zur Verfügung, die es in der Datenbank eventuell gar nicht gibt. Eine Dropdown-List kann die cList-Eigenschaft von EnumerationData nutzen, um alle zugelassenen Werte anzuzeigen. Abgespeichert wird dann immer nur der ausgewählte Wert oder ein Zeiger darauf (Index).

Für jedes Attribut des Business Objects, das mit einem Tabellenfeld verknüpft ist, wird ein DataField Object im BusinessObject aggregiert. Die BusinessObject Klasse muß also eine Containerklasse sein.

Die Anwendungsschicht

Beim Design der Anwendungsschicht weichen 3-Tier Applikationen in einigen Punkten vom gewohnten Aufbau einer Datenbankanwendung ab. 

ObjectLoader

Da die Aufgaben der Anwendung auf mehrere  bis viele BusinessObjects verteilt sind, kann man nicht davon ausgehen, daß alle benötigten Objekte jederzeit instantiiert sind. Deshalb sollte es einen ObjectLoader geben, der dafür zuständig ist, Objekte zu instantiieren. Ähnlich wie beim Aufruf von Forms über einen FormLoader fordert BusinessObject A über den ObjectLoader eine Instanz von BusinessObject B an. Wenn diese bereits im Hauptspeicher ist, wird eine Referenz darauf an A zurückgegeben. Andernfalls wird B zunächst instantiiert, wobei der ObjectLoader intern Referenzen auf alle durch ihn instantiierten Objekte verwaltet.

Childobjects

Ein BusinessObject kann ChildObjects haben. So wird ein BusinessObject „Rechnung“ wahrscheinlich „Rechnungspositionen“ als ChildObjects beinhalten. Die zu einem BusinessObject gehörenden ChildObjects bilden einen typischen ObjectSet. Eine Referenz auf den ObjectSet der ChildObjects wird in einer Eigenschaft des BusinessObjects festgehalten.

Locking über Semaphore

Bestimmte Aktionen im BusinessObject erfordern, daß das BusinessObject in einer Ausprägung (Identität, siehe oben) nur einmal vorhanden ist. So darf das BusinessObject „Rechnung Nr. 4711“ nicht an zwei Arbeitsstationen gleichzeitig editiert werden. Da ein Zugriff auf die physischen Daten nur über die logische Datenbank erfolgt, ist ein Satzschutz nicht (oder nur sehr schwer) zu realisieren. Deshalb werden solche Aktionen über Semaphore geschützt. Der zu setzende Semaphor darf dabei nicht nur die Aktion „Rechnung editieren“ berücksichtigen, sondern muß zusätzlich die Identität der zu editierenden Rechnung kennen. Dabei hilft die Verwendung von systemweit eindeutigen Object-IDs. Ein Semaphor Lock_Object_<Object_ID> reicht dann aus um ein Locking-Verfahren für alle BusinessObject-Klassen zu implementieren. 

Verbindung zur logischen Datenbank

3-Tier Applikationen und deren Komponenten arbeiten auf einer logischen Datenbank. Die Verbindung zwischen den Business-Klassen und den Tabellen der logischen Datenbank wird sinnvollerweise durch eine TableBehaviour-Klasse und deren Ableitungen geschaffen.

TableBehaviour

Eine solche Klasse übernimmt das komplette Tabellenhandling wie Requery() und Update() mit Behandlung von Update-Konflikten etc. In den Eigenschaften der Tablebehaviour-Klasse ist unter anderem festgehalten, mit welchem Alias die Tabelle (der View) geöffnet werden soll. Dieser Alias wird in der DataField-Klasse verwendet und auch in List-Controls der Oberfläche wie Grids, List- oder Comboboxes.

SingleRecordView

Die SingleRecordView-Klasse ist eine Ableitung aus TableBehaviour. Sie soll Einzelsatzviews bereitstellen, die die Daten genau eines BusinessObjects enthalten. Dazu hat die SingleRecordView-Klasse eine Eigenschaft vpID, die als Viewparameter dient. Vor dem Aufruf von Requery() wird hier die ID des gesuchten Objects eingetragen. Da Requery() eine (ererbte) Methode der SingleRecordView-Klasse ist, kann die Viewdefinition über THIS auf diesen Viewparameter zugreifen.

CREATE SQL VIEW ..... AS;

SELECT ....

....

WHERE .... == THIS.vpID

MultiRecordView

Die MultiRecordView-Klasse ist ebenfalls aus TableBehaviour abgeleitet. Da der zu Grunde liegende View normalerweise mehr als einen Datensatz enthält, gibt es hier Navigationsmethoden. Diese liefern als Rückgabewert jeweils die ID des angewählten Satzes. Viewparameter werden hier noch nicht gebraucht, sondern erst in der aus MultiRecordView abgeleiteten Klasse

ObjectSet

Eine ObjectSet-Klasse ist genau wie eine SingleRecordView-Klasse auf einen spezifischen View zugeschnitten. Sie enthält demnach so viele Viewparameter-Eigenschaften wie für die Viewdefinition gebraucht werden. Die Navigationsmethoden werden vom MultiRecordView geerbt. Die Eigenschaft oCurrent dient dazu, eine Referenz auf ein BusinessObject zu bieten, das dem aktuellen Satz des Views entspricht. Damit kann die ObjectSet-Klasse nicht nur zur Anzeige von ObjectSets sondern auch zum Bereitstellen von ChildObjects genutzt werden. Auch eine Verkettung von ChildObjects ist so möglich.

Bei der Implementation der ObjectSet-Klassen, die auch von Oberflächenelementen aus erreicht werden müssen, wird die Frage wichtig, in welcher Sprache mögliche Oberflächen zu einer Komponente erstellt werden könnten. Aus Visual Basic oder Delphi heraus können Sie nämlich nicht direkt auf einen VFP Viewcursor zugreifen. Wenn dies erforderlich ist, sollten Sie darüber nachdenken, diese ObjectSets als ADO-Objekte aufzubauen. 

Zusammenfassung

Der Aufbau einer VFP-Applikation in 3-Tier Technik bietet viele Vorteile. Allerdings entsteht auch ein Mehraufwand.

So muß nicht nur die zu Grunde liegende Datenbank aufgebaut werden, sondern es muß zu jedem BusinessObject ein SingleRecordView erstellt werden. Jeder benötigte ObjectSet muß ebenfalls als View abgebildet werden. Für alle diese Views müssen Klassen erstellt werden, ebenso DataField-Klassen für alle Felder der Viewcursor, die mit Attributen eines BusinessObjects verknüpft sind. Da diese Aufgaben projektspezifisch sind, können sie nicht einmalig im Framework abgehandelt werden. Dazu braucht man also Builder, die zum Beispiel aus einer Viewdefinition alle benötigten Klassen bis hin zur BusinessObject-Klasse aufbauen.

Da die in dieser Session vorgestellte 3-Tier Architektur ein geschlossenes Designprinzip darstellt, bietet sie allerdings auch die Chance, weitestgehend automatisierte Builder zu schreiben und einzusetzen. Noch effektiver wäre es natürlich, die benötigten Klassen automatisch aus einem Modell heraus aufbauen zu lassen.

Die oben dargestellten Vorteile der 3-Tier Architektur gelten vor allem wenn

Ich hoffe, Ihnen mit dieser Session einen Überblick über die Möglichkeiten und Vorgehensweisen beim Design von 3-Tier Applikationen in VFP geboten zu haben.