Session D-Top

Top-Down - ein Vorgehensmodell für objektorientes Applikations-Design

Manfred Rätzmann
raezz@t-online.de


Top Down

Wenn man vor der Aufgabe steht, ein neues Programm zu entwickeln, trägt man häufig einen ganzen Sack guter Vorsätze mit sich herum: "Diesmal machen wir das aber richtig!"

Die Segnungen der objektorientierten Programmierung haben wir verinnerlicht, das 3-Schichten Modell zumindest in der Theorie begriffen und für gut befunden, diverse eigene Klassen im Schrank, die wir natürlich wiederverwenden möchten.

Weil es sich so gehört, planen wir zu Beginn der Entwicklung eine längere Analyse und Designphase ein.

Nachdem einigermaßen klar ist, was der Kunde will (zumindest vorläufig), drängelt er aber bald darauf, erste Ergebnisse zu sehen. Da wir uns als erstes auf die Datenmodellierung gestürzt haben, gibt es auch schon einiges an Tabellen. Also werden schnell ein Anwendungsrahmen, Startprogramm, Menü, etc. zusammengestellt und ein paar Forms gebastelt - bevorzugterweise aus dem Bereich Stammdatenerfassung. Die gehen meistens recht fix und der Kunde ist zunächst mal ruhig gestellt.

Die neue Anwendung wächst und gedeiht, Form kommt zu Form, alles funktioniert auch einigermaßen (zumindest vorläufig) - aber irgendwann merken wir, dass wir wieder im alten Gleis fahren. Wieder steckt viel zu viel Funktionalität in einer einzelnen Form, die wir an anderer Stelle auch gut gebrauchen könnten. Diese jetzt da rauszulösen und in eine Klasse zu packen hieße aber, die ursprüngliche Form komplett umzubauen. Also wird wieder kopiert, hier was dazugebaut, dort eine Krücke geschaffen, usw. usf. bis wir wieder dort gelandet sind, wo wir nicht mehr hinwollten: Im Chaos.

Dass man eine etwas komplexere Datenbank nicht mit dem VFP Tabellendesigner ad hoc aufbauen sollte, sondern mit einem Modellierungstool, haben die meisten von uns eingesehen und halten sich auch daran. Aber eine ganze Anwendung modellieren, geht denn das überhaupt? Sollen wir etwa alle Forms zunächst in einem Modellierungstool darstellen, mit all ihren Controls und deren Click-, Valid-, SetFocus- und DoWhatEver- Methoden? Nur um die Form danach in VFP noch einmal zu erstellen? Und jede Änderung im Modell wieder nachzupflegen? Schwer vorstellbar. Wann soll denn das fertig werden?

Zweifelsohne ist die fehlende Verknüpfung zwischen Entwicklungsumgebung und Modellierungswerkzeug das gravierendste Hemmnis bei der Umsetzung all unserer guten Vorsätze. Irgendwann werden die Produzenten der Entwicklungswerkzeuge nicht mehr umhin können auch die Modellierung einzubeziehen und das zu ermöglichen, was man "Round Trip Engineering" nennt. Und bis dahin? Da das Weiterwurschteln wie bisher keinen rechten Spaß mehr macht sollten wir uns eine Vorgehensweise überlegen, mit der wir unseren Vorstellungen näher kommen, die aber auch praxistauglich ist.

Zwei Fragen gibt es dabei zu beantworten:

  1. Was sollte sinnvollerweise modelliert, das heißt, an Hand eines Klassenmodells oder ähnlichem durchdacht werden?

  2. Wie bauen wir die Modellierung in unsere Entwicklungspraxis ein, die ja auch von den Wünschen des Kunden, Notwendigkeiten im Projektablauf und anderen externen Faktoren bestimmt wird?

Da wir nach einer praxistauglichen Vorgehensweise suchen, wollen wir die Beantwortung dieser Fragen auch gleich an einem kleinen Beispiel überprüfen.

Die Anwendung

Es soll ein Programm für eine Kleintierpraxis erstellt werden. Das Programm soll:

Das ist nicht alles, was ein Programm für eine Kleintierpraxis können muß, aber für unser Beispiel reicht schon der erste Punkt, die Karteiführung.

Der erste Schritt bei unserer Vorgehensweise ist eine Aufgabenbeschreibung.

Die Aufgabenbeschreibung

Die Aufgabenbeschreibung entsteht aus den Vorgaben des Auftragsgebers. In ihr wird in natürlicher Sprache (d.h. nicht formalisiert) festgehalten, welche Zielsetzung das zu entwickelnde Programm hat, in welcher Umgebung es eingesetzt werden soll und welche Ergebnisse erwartet werden. Neben den rein sprachlichen Dokumenten kann ein erstes Modell (Geschäftsmodell) Bestandteil der Aufgabenbeschreibung sein. Das Geschäftsmodell wird als Entity-Relationship Diagramm (Klassenmodell ohne Klassenmethoden oder logisches Datenmodell) erstellt und kann bei Bedarf um Aktivitätsdiagramme, Sequenzdiagramme oder andere Systemdiagramme ergänzt werden. Die Aufgabenbeschreibung wird vom Auftraggeber abgenommen.

Was uns als Modellierer der neuen Anwendung zu diesem Zeitpunkt besonders interessieren sollte sind das Geschäftsmodell und die grobe Aufgabenunterteilung.

Das Geschäftsmodell

Das Geschäftsmodell dient dazu, den Geschäftsbereich unseres Kunden, in dem das Programm eingesetzt werden soll, besser zu verstehen. Es basiert auf Interviews, die wir mit unserem Kunden führen. Hier ein Ausschnitt aus dem Geschäftsmodell der Kleintierpraxis:

Daten sammeln

Datenbankentwickler denken meistens schon sehr früh in Datenstrukturen. Daran sollten Sie sich auch nicht hindern lassen. Wohin also mit den Informationen, die der Kunde uns zu den gewünschten Daten gibt? Legen Sie diese am besten zunächst als Attribute im Geschäftsmodell ab. Zum Aufbau eines richtigen Datenmodells ist es noch viel zu früh und die ersten Informationen zu gewünschten Daten sind in der Regel Kerndaten der jeweiligen Geschäftsobjekte.

Aufteilung in Module

Die grobe Aufgabenunterteilung ist ein erster Hinweis darauf, in welche Module (oder Subsysteme) wir das Gesamtprogramm zerlegen können. Diese erste Aufteilung ist ein ganz wichtiger Schritt, da sie entscheidet, wie wir Funktionalität bündeln oder separieren.

Module der neuen Anwendung (Ausschnitt):

UseCase Analyse

Zur Vorbereitung des Oberflächen-Entwurfs führen wir eine UseCase Analyse durch.

In der UseCase Analyse wird ermittelt, wie die zukünftige Anwenderin das System sieht, das heißt, welche Eingaben oder Aktionen sie durchführt und welche Ergebnisse sie erwartet. Auch die UseCase Analyse stützt sich im wesentlichen auf Interviews mit den zukünftigen Anwendern. Ausgehend von der Aufgabenbeschreibung wird detailliert jede Anwendungssituation analysiert und beschrieben. Ergebnis der Analyse sind UseCases als Dokumente und in Diagrammform. Die UseCases sind Grundlage für das weitere Design und dienen später als Checklisten beim Systemtest. Sie sollten, soweit möglich, von den zukünftigen Anwendern überprüft und bestätigt werden. Häufig ist dies aber erst sinnvoll möglich, wenn der Oberflächen-Prototyp vorliegt.

Hier als Beispiel ein UseCase Diagramm zu unserer Lösung für die Kleintierpraxis:

Oberflächen Entwurf

Wenn wir das Gesamtprogramm in Module zerlegt und die gewünschte Benutzung in UseCases niedergelegt haben, ist der Moment gekommen, einen Oberflächen Prototypen zu erstellen. Damit können wir zum einen unsere Modulaufteilung überprüfen, zum einen zeigt uns der Prototyp auch, wie die Schnittstellen der einzelnen Module beschaffen sein müssen. Außerdem können wir dem Kunden damit in dieser frühen Entwicklungsphase bereits zeigen, wie sein Programm später aussehen wird.

Prototyp bedeutet nicht, dass die Oberfläche nur skiziert wird. Sie sollte vielmehr im Detail ausgearbeitet werden, da dort bekanntermaßen der Teufel steckt (im Detail nämlich). Prototyp bezieht sich vielmehr auf die Tatsache, dass

Mit diesem Prototypen wollen wir möglichst viele der Anforderungen an unsere Klassen frühzeitig aufdecken. Wir verwenden unser Entwicklungssystem mit seinen hervorragenden RAD (Rapid Application Development) Fähigkeiten also als Oberflächen-Modellierungstool. Das hat den entscheidenden Vorteil, dass wir die einmal modellierte Oberfläche zu großen Teilen für die eigentliche Anwendung weiter verwenden können.

Wenn die gleiche Anwendung unter verschiedenen Oberflächen laufen soll zum Beispiel einmal als Stand-Alone Applikation und einmal als Browser-gesteuerte Web-Applikation, empfiehlt es sich, beide Oberflächen zu modellieren, das heißt, mit dem jeweiligen Entwicklungstool als Prototyp aufzubauen. Dadurch wird schon vor der Entwicklung der eigentlichen Anwendungsklassen klar, welche gesonderten Techniken die jeweilige Oberfläche erfordert.

Oberflächenentwurf zu unserer Beispielapplikation

Objektmodelle der Module

Der Oberflächenentwurf zeigt uns nicht nur, welche Funktionen wir wo brauchen  also, ob unsere Aufteilung in Module sinnvoll ist sondern auch, welche Schnittstelle die einzelnen Module bieten müssen, um die Oberfläche bedienen zu können. Jedes Modul kann als ein Objekt angesehen werden, das Methoden anbietet, die von der Oberfläche aufgerufen werden. Außerdem bietet ein Modul eine interne Struktur von Unterobjekten, die auf der Oberfläche angezeigt werden können.

Wer sich schon mal mit der Einbindung von OLE-Servern in sein Programm befasst hat weiß, dass die wichtigste Dokumentation zu einem OLE-Server dessen Objektmodell ist. Deshalb entwerfen wir für unsere Module ebenfalls Objektmodelle, die sich aus den Anforderungen des Prototyps ergeben.

Die Karteiverwaltung stellt demnach ein Listenobjekt zur Verfügung (Karteikartenliste) und ein Karteikartenobjekt. Die beiden sind nicht miteinander verknüpft, das heißt, wenn die Karteikartenliste auf einen anderen Eintrag gesetzt wird, wird das Karteikartenobjekt nicht automatisch mit umgesetzt.

Das Karteikartenobjekt selbst bietet eine Eintragsliste an und ein Eintragsobjekt. Auch hier besteht keine automatische Verknüpfung zwischen der Liste und dem Eintragsobjekt.

Das Eintragsobjekt bietet die Attributobjekte "Datum", "Text" und "Klasse" an sowie ein "Details" Objekt, das je nach der Klasse des Eintrags unterschiedliche Attribute hat.

Außerdem bietet das Karteikartenobjekt den Zugriff auf das jeweils aktuelle Kundenobjekt und das Patientenobjekt. Da Kunden und Patienten immer nur über die Karteiverwaltung gepflegt werden, sind die jeweiligen Verwaltungsmodule in die Dateiverwaltung integriert.

Designmodell

Im nächsten Schritt überlegen wir uns, welche Klassen wir brauchen, um dieses Objektmodell zur Laufzeit verfügbar zu machen. Aus diesen Überlegungen entsteht das Designmodell der Anwendung.

Auch beim Designmodell gehen wir am besten TOP DOWN vor, das heisst, wir modellieren zunächst die Modul Klassen selbst um uns deren Schnittstellen klar zu machen. Danach kümmern wir uns um die Bestandteile der Module.

Für alle Module bilden wir eine abstrakte Basisklasse und leiten aus ihr die ebenfalls abstrakte Klasse "DataManager" ab, die für Datenverwaltungs-Module gedacht ist. Diese wird schon mit den wesentlichen Methoden bestückt sodass in den konkreten Klassen nur noch die Methoden hinzugefügt werden müssen, die für ein bestimmtes Datenverwaltungs-Modul spezifisch sind. Beim "Karteimanager" Modul ist das zum Beispiel die Methode "BesitzerWechsel".

Die Methoden First(), Last(), Next() und Previous() der DataManager Klasse setzen die Karteikartenliste auf den angeforederten Satz und laden das dazu gehörende Karteikartenobjekt. LoadCurrent lädt das Karteikartenobjekt des Satzes, auf dem die Liste gerade steht. Über LoadCurrent kann also das jeweils aktuelle Karteikartenobjekt geladen werden, wenn der Zeiger der Karteikartenliste programmatisch oder zum Beispiel durch einen Klick auf eine Zeile in dem Grid, das die Liste anzeigt, bewegt wird.

Anbindung der Oberfläche

Damit wären wir bei der Frage, wie die Oberfläche an diese Klassen angebunden wird. Bei einem Doppelklick auf eine Zeile des Karteigrids soll die entsprechende Karteikarte geladen und angezeigt werden.

Bei einem Doppelklick auf das Grid wendet sich dieses an die KarteiverwaltungForm, in die es eingebettet ist und ruft deren Methode "OeffneAktuelleKarteikarte" auf. Die Form agiert hier also als Workflow-Manager und bietet für die einzelnen Workflows jeweils eine eigene Methode an.

Die Form ruft die Methode "LoadCurrent" des mit ihr verknüpften Karteimanagers auf. Dabei wird die ID der aktuellen Karteikarte zurückgegeben, die aber nicht weiter verwendet wird.

Anschließend ruft die Form den FormLoader auf, ein Objekt also, das nur dazu da ist, Forms zu laden und anzuzeigen. Der FormLoader wird aufgefordert, die Form "Karteikarte" anzuzeigen und gibt eine Referenz auf die angezeigte Form zurück.

Mit dieser Referenz kann nun die KarteikartenForm direkt angesprochen werden. Deren "WorkOn" Methode wird aufgerufen, wobei eine Referenz auf das Karteiverwaltungsobjekt übergeben wird. Dieses wird addressiert mit Thisform.Karteiverwaltung entsprechend des oben angegebenen Objektmodells der Karteiverwaltung. Damit weiss die Karteikartenform auf welchem Karteiverwaltungs-Objekt sie arbeiten soll und zeigt die Attribute des darin enthaltenen Karteikarten-Objekts an.

Ein weiteres Beispiel:Was soll in der Karteiverwaltung passieren, wenn die Anwenderin auf den Toolbarbutton "Neu" klickt?

Die Toolbar ruft die Methode "NeueKarteikarte" der KarteiverwaltungForm auf, diese gibt den Aufruf weiter an das mit ihr verknüpfte Karteiverwaltungsobjekt. Die Karteiverwaltung ruft zunächst Kundenmanager.New() und dann Patientenmanager.New() auf. Dadurch werden ein neues Kundenobjekt und ein neues Patientenobjekt angelegt aber noch nicht gespeichert. Beide Methoden geben jeweils die ID des neuen Objektes zurück. Diese beiden Ids werden dann von der Karteiverwaltung beim Aufruf der Methode Karteikarte.New() an das Karteikartenobjekt übergeben.

Später wird in der Toolbar der Speichern Button angeklickt. Die "Speichern" Anforderung wird von der Form an die Karteiverwaltung und von dort an den Kundenmanager, den Patientenmanager und schließlich die Karteikarte weiter gegeben. Wenn zum Beispiel das Kundenobjekt aus irgend einem Grund nicht gespeichert werden kann und die Save() Methode deshalb ein .F. zurückgibt kann die Karteiverwaltung verhindern, dass die Patientendaten und die Karteikarte gespeichert werden.

An dieser Stelle, beim Designmodell stecken wir also schon ziemlich tief im Detail. Mit Sequenzdiagrammen wie den beiden oben gezeigten haben wir ein Mittel an der Hand mit dem wir Abläufe unseres neuen Programms verifizieren können bevor überhaupt irgend etwas codiert ist. Natürlich wird nicht jeder Ablauf im Programm in einem Sequenzdiagramm dargestellt. Wenn wir einen Ablauf dargestellt und für gut befunden haben gehen wir davon aus, dass dieser Ablauf bei den anderen Klassen gleichen Typs - z.B. bei allen von DataManager abgeleiteten Klassen - genauso funktioniert.

Das gleiche gilt, wenn wir noch eine Stufe tiefer gehen und vom Designmodell zum Implementationsmodell fortschreiten

Implementationsmodell

Hier beschäftigen wir uns mit der Frage, wie wir die im Designmodell dargestellt Applikation tatsächlich implementieren wollen. Welche Basisklassen benutzen wir für die im Designmodell gefundenen Klassen? Wie binden wir die Designklassen an das von uns benutzte Framework an? Wie werden die Textboxes der Oberfläche mit den Objektattributen verknüpft? usw. usf.

 

Die Verbindung zur Datenbank wird in unserer Beispielapplikation über eine TableBehaviour Klasse hergestellt. Diese kümmert sich um Update 
und Delete einzelner Datensätze und führt 

auch das Requery durch, wenn sie einen View repräsentiert.

Wir arbeiten mit zwei Arten von Views. Zum einen Views, die immer nur einen einzelnen Datensatz enthalten. Darauf arbeitet die Klasse PersistentObject. Zum anderen Views, die mehrere Datensätze als Gruppe von Objekten enthalten. Solche Views werden durch Abkömmlinge der Klasse ObjectSet repräsentiert.

Ein Beispiel dafür ist die Klasse Karteikartenliste. Von Tablebehaviour erbt die Karteikartenliste das Attribut cAlias. An diesen Alias wird dann das Grid gebunden, das die Karteikartenliste anzeigen soll.

Wie das geht, zeigt der folgende Codeabschnitt

Alle Objekte, die von PersistentObject abgeleitet sind, enthalten Unterobjekte der Klasse PersistentAttribut und zwar pro Attribut, das gespeichert werden soll, eins. Diese Objekte der Klasse PersistentAttribut sind dafür verantwortlich, das jeweilige Attribut aus dem Datensatz, der sich hinter PersistentObject verbirgt, zu lesen bzw. es dort wieder zu speichern.

Für unterschiedliche Datentypen werden Ableitungen von PersistentAttribut benutzt, die genauer auf den jeweiligen Datentyp eingehen, indem sie zum Beispiel Min und Max Werte oder die Anzahl von Nachkommastellen bei numerischen Attributen zur Verfügung stellen.

Bei der Kunden Klasse, die von PersistentObject abgeleitet ist, sieht man, dass die dort aufgeführten Attribute alle einen Typ haben, der einer Ableitung von PersistentAttribut entspricht.

Die Klasse PersistentAttribut stellt die Methoden GetValue() und SetValue() zur Verfügung, mit denen man eine Textbox in einer Form einfach an das Attribut anbinden kann.

Im Refresh Event der Textbox steht dazu:

und im Valid Event der Textbox steht:

Damit haben Sie die Textbox an das Nachnamen-Attribut des Kunden der aktuellen Karteikarte angebunden.

Außerdem sehen Sie hier gleich, warum Sie unbedingt ein stimmiges Objektmodell ihrer Module brauchen.

Datenmodell

Mit dem Designmodell, spätestens aber mit dem Implementationsmodell haben Sie alle Informationen zur Hand um die Datenstruktur erstellen zu können, die Ihr Programm ganz unten stützt. In meiner Session D-Clare erzähl ich was darüber, wie aus einem Klassenmodell ein Datenmodell werden kann. Deshalb will ich dieses Thema hier nicht vertiefen.

Zusammenfassung

Das führt uns zurück zu den beiden Fragen, die wir am Anfang gestellt haben:

Modellierung ist kein Selbstzweck. Da es nicht sinnvoll ist, Dinge zweimal zu tun, sollten Sie nichts modellieren, was Sie ebenso gut im Code abbilden können.

Nutzen Sie die Modellierung dazu, sich kritische Strukturen und Abläufe klarzumachen und diese festzuhalten, bevor Sie mit dem Kodieren beginnen. Dazu gehört sicherlich das Geschäftsmodell, wichtige UseCases, aber vor allem die Modulaufteilung und die Objektmodelle der einzelnen Module.

Danach sollten die wesentlich an der Anwendungsarchitektur beteiligten Klassen im Designmodell und Implementationsmodell durchdacht werden.

Mein Traum wäre ein Modellierungstool, aus dem heraus man, nach der Erstellung des Implementationsmodells, den ersten Code automatisch generieren könnte und das im weiteren Verlauf der Kodierung zur Dokumentation der im Code verborgenen Strukturen und Zusammenhänge im Modell dienen könnte.

In der ersten Phase, der Aufgabendefinition, ist das Geschäftsmodell ein hervorragendes Mittel zur Kommunikation mit dem Auftraggeber. Auch UseCase Modelle und näher erläuternde Aktivitätsdiagramme können dazu dienen, die Aufgabe besser zu verstehen und sich genauer mit dem Auftraggeber abstimmen zu können.

Der wesentliche Punkt meiner TOP DOWN Strategie ist aber, dass Sie sehr frühzeitig mit der Modellierung der gesamten Anwendungsoberfläche beginnen. TOP DOWN heißt also nicht nur, in den modellierten Details vom Groben zum Feinerenfortzuschreiten sondern auch, das Programm selbst von oben - der Oberfläche - über die mittlere Schicht - den Business Objekten - nach unten - zur Datenhaltung hin zu entwickeln. Da es für die Oberfläche kein besseres Modellierungstool gibt als die jeweilige Entwicklungsumgebung, ist das Erstellen der kompletten Oberfläche gleich am Anfang des Modellierungsprozesses kein Methodenbruch sondern eine logische und sehr effektive Vorgehensweise.