Objektorientiertes Design
Geschwurbel von Daniel Schwamm (09.04.1994)
Inhalt
In bisherigen Modellierungsprozessen wurden unterschiedliche
Techniken in jeder Phase verwendet. So wurden z.B. für die Analyse Entity
Relationship-Modelle (ERM), Datenflussdiagramme (DFD) und
Zustandsdiagramme verwendet, während in der Design-Phase normalisierte
Tabellen und Structure Charts zum Einsatz kamen. Auch die abschliessende
Programmierung bzw. Implementierung verlangte ein Umdenken der Modellierer
dahingehend, dass die Designergebnisse erst an die
Implementierungstechniken angepasst werden mussten.
Bei einem objektorientiertem Ansatz kommt es nicht zu
Brüchen beim Übergang von einer Phase in die andere. Das
objektorientierte Design (OOD), welches ein Teil der objektorientierten
Entwicklung (OOE) repräsentiert, bietet damit grundsätzlich zwei
Vorteile gegenüber herkömmlichen Modellierungsmethoden:
-
Das OOD-Modell kann die Semantik des Problembereichs
besser abbilden als seine Vorgängermodelle.
-
es besteht Konsistenz zwischen den Modellierungsphasen
objektorientierte Analyse (OOA), OOD und objektorientierte Programmierung
(OOP). Aber wodurch wird diese Konsistenz erreicht? Das OOA-Modell ist direkt
sukzessiv erweiterbar mit den Methoden des OOD, und die verwendete Terminologie
ist in allen OOE-Phasen die gleiche.
Was zeichnet nun eine objektorientierte Modellierung aus?
Sehen wir uns dazu die folgenden Kriterien an, die für alle
objektorientierten Vorgänge gelten:
-
Daten und Funktionen werden als Einheiten gesehen (die
zusammen in Objektklassen verpackt werden). Bei den herkömmlichen Methoden
wurden die Daten und ihre Strukturen z.B. in einem ERM modelliert, während
die Funktionen getrennt davon in Flussdiagrammen grafisch dargestellt
wurden.
-
Allgemeinen Strukturen können explizit betont werden.
Dies wird dadurch erreicht, dass die allgemeinen Strukturen (Objekte)
ihrer Eigenschaften an andere Strukturen weitervererben können. Nur die in
der abgeleiteten Klasse verwendete Semantik muss explizit erklärt
werden, während die vererbten Methoden nicht neu zu codieren sind. Die
Vererbung erlaubt eine stufenweise Verfeinerung des Modellentwurfs, setzt aber
gute Kenntnisse des Problembereichs voraus. Um die Vererbungstechnik effizient
auszunutzen, sollten alle Methoden und Attribute so niedrig als möglich
und so hoch als nötig in der Hierarchie angeordnet sein.
-
Die OOE erlaubt es, Module wiederzuverwenden und einzeln
abzuändern, wodurch eine Evolution bestehender Systeme wesentlich
erleichtert wird. Dadurch steigert sich nicht nur die Produktivität der
OOP-Ergebnisse, sondern auch deren Qualität.
-
Besonders der letzte Punkt macht deutlich, dass sich
das Risiko einer grossen SW-Entwicklung unter Einsatz von OOE-Methoden
minimiert, da stets Teile davon für andere Programme verwendet werden
können und nachträgliche Änderungen keinen Neuentwurf
verlangen.
Die OOA-Phase ist der OOD-Phase vorgelagert, d.h. die
OOD-Phase arbeitet mit den Ergebnissen der OOA-Phase unter anderer Zielsetzung
weiter. Die Zielsetzung der Analyse war es, zu untersuchen, WAS für
Vorgaben zu erfüllen sind, um einen Problembereichs bearbeiten zu
können. Die technischen Rahmenbedingungen werden dabei völlig
unterschlagen, denn diese sind erst Thema der OOD-Phase. Hier gilt die
Zielsetzung: WIE sind die Vorgaben der OOA zu erfüllen? Dabei richtete
sich die OOD nach der Strategie, so wenig als möglich neu zu entwickeln und
so viel als möglich wiederzuverwenden. Als Ergebnis der OOD erhält man
genaue Spezifikationen der neu zu entwickelnden Komponenten und zudem Vorgaben,
inwieweit bestehende Komponenten angepasst und verwendet werden
können, um das gegebene Problem lösen zu können. Die
Implementierung mittels einer OOP ist dann nur noch ein kleiner Schritt.
Sehen wir uns zunächst einmal an, was die OOA für
Arbeitsschritte durchläuft, wobei wir von dem Fünf-Schichten-Modell
von Coad und Yourdon ausgehen wollen. Die Subjekt-, Klassen-, Struktur-,
Attribut- und Methoden-Schichten werden mit den folgenden fünf Schritten
modelliert:
-
Festlegung der Objekte und Klassen
Objekte sind Abbildungen der realen Welt, die über
Zustände (Attributsausprägungen) und Verhalten (anwendbare Methoden)
beschrieben werden können. Stimmen Objekte mit anderen Objekten in
Verhalten und Datenstrukturen überein, so lassen sich aus ihnen Klassen
bilden. Ein Beispiel für zwei Objekte, die sich zu einer Klasse
"sprechende_Ente" zusammenfassen lassen, sind "Dagobert Duck" und "Donald
Duck".
-
Identifizierung der Vererbungsstrukturen
Wenn eine Klasse einer anderen Klasse bestimmte Eigenschaften
vererbt, dann spricht man von einer "is a"- bzw. "kind of"-Beziehung oder einer
Generalisierungsstruktur bzw. Spezialisierungsstruktur zwischen diesen beiden Klassen.
Die vererbende Klasse nennt man Basisklasse und die erbende Klasse abgeleitete
Klasse. Durch Instanziierung der abgeleiteten Klasse wird automatisch auch die
zugehörige Basisklasse instanziiert. Ein Beispiel für solch eine
Struktur ist zwischen den Klassen "sprechende_Ente" und
"sprechende_geizige_Ente" gegeben, d.h. unter sprechenden Enten gibt es auch
welche, die geizig sind.
In C++ können Elementfunktionen (Methoden) einer Klasse
auf drei Arten "is a"-vererbt werden:
-
Vererbung nur des Interface: die Basisklasse enthält
nur "pure virtual"-deklarierte Methoden ohne Implementierung; sie ist also
abstrakt. Die Methoden MÜSSEN von der abgeleiteten Klasse definiert
werden.
-
Vererbung des Interfaces und Default-Implementierungen: die
Basisklasse enthält virtuelle definierte Methoden, die die abgeleitete
Klasse benutzen oder durch eigen überschreiben kann.
-
Vererbung des Interfaces und der Implementierungen: die
Basisklasse enthält nicht-virtuelle definierte Methoden, die die
abgeleitete Klasse nicht durch eigene überschreiben sollte.
Baut sich eine Klasse aus Eigenschaften anderer Klassen auf,
dann besteht zwischen diesen Klassen eine "part of"-, "has a"- bzw. "is
implemented in terms of"-Beziehung oder eine Whole-Part- bzw.
Aggregation-/Zerlegungsstruktur. Diese Struktur kann man in C++ durch Layering
erreichen, d.h. die Aggregat-Klasse enthält die Zerlegungsklasse als
Attribut, einen Pointer darauf oder als geschachtelte Klasse (Nested Class).
Eine Whole-Part-Struktur besteht z.B. zwischen den Klassen "Supermarkt" und
"Ravioli", d.h. jeder Supermarkt enthält verschiedene Sorten Ravioli.
Objekte können auch miteinander in Beziehung stehen, ohne
dass es sich dabei um eine Vererbungsstruktur handeln muss (auch wenn
sie in C++ wie Whole-Part-Strukturen durch Layering implementiert werden). Die
aus dem ERM bekannten Beziehungen lassen sich folgendermassen
implementieren (es ist jeweils eine Vorwärtsdeklarationen nötig):
-
1:1-Beziehung: Zeiger in beiden Klassen.
-
1:M-Beziehung: Zeiger in einer Klasse, Liste (Anzahl
unbekannt) oder Feld (Anzahl bekannt) in der anderen Klasse.
-
N:M-Beziehung: 1:M-Beziehungen in beiden Klassen.
- Identifizierung der Subjekte
- Definition der Attribute
- Definition der Methoden
Sehen wir uns jetzt an, was die OOD für Arbeitsschritte
durchläuft, wobei wir uns an das Vier-Komponenten-Modell von Coad und
Yourdon halten wollen. Diese vier Komponenten sind im Einzelnen:
-
Problembereichskomponente: beim Design der
Problembereichskomponente werden die Ergebnisse der OOA um designspezifische
Aspekte erweitert.
-
Datenmanagement-Komponente: Abstimmung der Ergebnisse auf
das gewünschte Ergebnis. Hierunter fällt insbesondere die Verwaltung
persistenter Objekte.
-
Benutzeroberflächen: Anpassung der Benutzeroberfläche zur Kommunikation mit
dem späteren Endbenutzer.
-
Task-Management-Komponente: Diese Komponente designt die nötige
Koordination der einzelnen Tasks bei Multitasking-Systemen.
Normalerweise gibt es nicht nur einen Weg, ein (durch OOA) gut
analysiertes und spezifiziertes Problem zu designen. So muss z.B. eine
Wahl getroffen werden, ob man eine objektorientierte Sprache wie C++ oder eine
funktionale Sprache wie PASCAL verwenden möchte, ob man grafische
Oberflächen einsetzen will, ob man multiple Vererbung benötigt, oder
ob man dem Modell eine relationale, hierarchische oder netzwerkartige Datenbank
zugrunde legen will. Auch die Verwendung von statischen (Feldern) oder
dynamischen Strukturen (Listen) muss entscheiden werden, genauso wie die
Modellierung einzelner Eigenschaften von Klassen als Attribute oder
eigenständigen neuen Klassen.
Hat man sich für einen Designer-Weg entschieden, kann
dieser durch eine einheitliche Dokumentation der Design-Entscheidungen
formalisiert werden. Insbesondere bei komplexen Systemen gewinnt das formale
Design an Bedeutung. Es muss daher:
- übersichtlich/leicht erlernbar sein - z.B. durch grafische Darstellung.
- alle für die nächsten Schritte relevanten Informationen enthalten.
- durch CASE-Tools unterstützbar sein.
Dadurch wird ein "gemeinsamer Nenner" geschaffen, wodurch auch
ausserhalb des Entwicklungsteams stehende Personen mit den
Designergebnissen umgehen können. Leider wird bisher auf ein formales
Design wenig Wert gelegt, da der Aufwand dafür trotz CASE-Tools erheblich
ist und die Ergebnisse so wenig greifbar.
Nach Coad und Yourdon drückt sich gutes Design v.a. darin
aus, dass es hilft, die anfallenden Kosten eines Systems während
seiner gesamten Lebensdauer insgesamt zu minimieren - auch wenn die Entwicklung
selbst etwas teuer gewesen sein sollte. Solche Kosten fallen an:
- bei der Analyse und dem Design des Systems
- bei der Umsetzung des Designs (Implementierung)
- bei den nötigen Tests
- bei den Betriebskosten des Systems (evtl. auch Anschaffungskosten!)
- und v.a. bei der Wartung des Systems.
Das Design sollte die allgemein geltenden Ziele der
SW-Entwicklung nicht übersehen. Sogenannte SW-Metriken helfen, zu
überprüfen, in wieweit die folgenden Ziele erreicht wurden:
- Korrektheit/Überprüfbarkeit
- Robustheit in Ausnahmefällen
- Erweiterbarkeit/Änderbarkeit (insbesondere grosser Systeme)
- Wiederverwendbarkeit
- Integrierbarkeit
- Kompatibilität (mit ähnlichen Produkten)
- Effizienz
- Portabilität
- Benutzerfreundlichkeit
- Wartungsfreundlichkeit
Durch die Modularisierungsmöglichkeiten von OOP, die eng
mit dem Klassenkonzept gekoppelt sind, wird ausserdem das Ziel erreicht,
die Abhängigkeit zwischen einzelnen Modulen möglichst gering zu
halten (Weak Coupling), die im Modul zusammengefassten Strukturen aber
sehr stark zusammenhängen zu lassen (High Cohesion). Darüber hinaus
ermöglichen Module es, die Implementierungsdetails vor den Anwender zu
verbergen, und ihnen nur eine (Standard-)Schnittstelle zur Verfügung zu
stellen (Information Hiding). Und zu guter Letzt gestatten Module noch die
Wiederverwendbarkeit von Systemteilen, besonders wenn Klassenbibliotheken
angelegt werden aus Klassen, die geschlossen für Veränderungen, aber
offen für Erweiterungen sind (Open-Closed-Prinzip). Diese Ziele des
SW-Designs sehen wir uns nun in den nächsten Abschnitten noch einmal
einzeln an.
Wie im Abschnitt zuvor erklärt wurde, erlaubt es das
Modulkonzept von OOP, dass Coupling, das ist die äussere
Verflechtung (keine Vererbungsstruktur!) von Klassen bzw.
Bibliotheksfunktionen, möglichst gering zu halten. Dies ist v.a. deswegen
wichtig, damit es nicht zu einem sogenannten Domino-Effekt kommt, wenn man eine
Klasse ändert: ist das Coupling stark ausgeprägt, folgt aus einer
Änderung der Methodenaufrufe einer Klasse eine Kettenreaktion von
Änderung an anderen Klassen, die diesen Methodenaufruf verwenden. Um den
Domino-Effekt zu minimieren, sollte Folgendes beachtet werden:
- Interface vorausschauend designen.
- Datenkapselung (v.a. Zugriffsrechte) konsequent betreiben.
- Auf friend-Funktionen verzichten (bei "cout"-Überladung aber nötig).
Je mehr Informationen zwischen den Objekten/Modulen
übermittelt werden müssen, desto höher ist der Coupling-Grad des
Systems. Die äussere Verflechtung in Bezug auf OOD kommt auf
dreierlei Arten zustande:
-
Verflechtung von Objekten durch Methodenaufrufe: ein
Objekt sollte so wenig Nachrichten empfangen bzw. senden wie möglich und
so viele wie nötig. Nach Möglichkeit sollten pro Methodenaufruf nicht
mehr als drei Argumente übergeben werden. Es ist z.B. nicht sinnvoll, wenn
die Methode der Klasse A eine Methode der Klasse B aufruft, um eine Methode der
Klasse C zu aktivieren, wenn die Methode der Klasse C auch direkt von der
Klasse A aufgerufen werden kann.
-
Verflechtung von Objekten durch Objekt-Beziehungen: ein
Objekt sollte so wenig Objekt-Beziehungen zu anderen Objekten haben wie
möglich und so viele wie nötig.
-
Verflechtung von Klassen durch Whole-Part-Strukturen: das
Layering sollte so gering gehalten werden wie möglich und so stark wie
nötig.
Im Gegensatz zum Coupling, das zu minimieren ist, ist die
Cohesion, also der innere Zusammenhang der Strukturen eines Objektes/Moduls, zu
maximieren. Jeder Teil des Ganzen soll eine wohldefinierte Aufgabe leisten. In
Bezug auf OOD ist Cohesion unter drei Aspekten zu beachten:
-
Cohesion einer Methode: eine Methode sollte genau eine
Aufgabe erfüllen und nicht mehr. Solche Methode kennzeichnen sich durch
zwei Kriterien: ihre Implementierung verlangt nur wenige Zeilen Code, und ihre
Aufruf lässt sich mit einem imperativen Satz aus Verb+Objekt
beschreiben (z.B. Fuelle_Feld).
-
Cohesion einer Klasse: in einer Klasse sollten nur die
Eigenschaften modelliert sein, die problemrelevant sind. Von Fuzzy-Definitionen
ist Abstand zu nehmen. So hat es z.B. wenig Sinn der Klasse "Auto" das Attribut
"Fluggeschwindigkeit" oder die Methode "Starte_Paarungsphase" zuzuweisen.
-
Cohesion einer Vererbungsstruktur: eine abgeleitete Klasse
sollte möglichst alle virtuellen Elementfunktionen und Attribute der
Basisklasse sinnvoll benutzen können, darüber hinaus aber auch eigene
Methoden und Attribute besitzen. In Zukunft werden sich auch selektive
Vererbungen realisieren lassen, die zwar dem "is a"-Gedanken widersprechen und
zu Unübersichtlichkeiten führen können, die abgeleiteten Klassen
aber vor unnötigem Basismethoden-Ballast schützen.
Um die Qualität eines OOD zu beurteilen, eigenen sich
neben der "Messung" des Coupling- und Kohäsionsgrades die folgenden
Methoden:
- Überprüfung der Module auf Wiederverwendungseignung
- Überprüfung, ob ein einheitliches/ausdrucksstarkes Vokabular benutzt wurde
- Überprüfung, ob CASE-Tools zum Einsatz kamen
- Die maximale Ebene der Vererbungsstrukturen bestimmen (<8?)
Wiederverwendbarkeit ist ein wichtiges Thema der OOE, daher
sehen wir sie uns noch einmal näher an. Die OOE sieht nicht nur vor
Programme zu schaffen, sondern auch deren Module so allgemein zu entwerfen,
dass sie sie auch noch in anderen Programmen verwendet werden
können. Dadurch mindert sich der Entwicklungsaufwand ganz erheblich. Wie
schon erwähnt eignen sich hierfür besonders Klassenbibliotheken, die
nach dem Open-Closed-Prinzip konzipiert werden.
Eine SW-Metrik ist ein Mass zur Quantifizierung von
Eigenschaften von Programmen (insbesondere der Programmkomplexität),
ausgedrückt als reelle Zahl. Meistens werden sie anhand des Programmtextes
bestimmt, wobei v.a. Programmlänge und Schnittstellen berücksichtigt
werden. SW-Metriken dienen dazu:
- die oben vorgestellten Ziele der SW-Entwicklung zu überprüfen.
- Wartbarkeit/Fehleranfälligkeit/Aufwand der Systeme abzuschätzen.
- Kosten und Terminplanung eines SW-Projektes einzuschätzen.
- die Produktivitätssteigerungen durch Tools zu beurteilen.
- Produktivitätstrends im Zeitablauf nachzuweisen.
- die SW-Qualität zu verbessern.
- zukünftigen Personalbedarf eines Projektes abzuschätzen.
- zukünftige Wartungsmassnahmen zu reduzieren.
Doch SW-Metriken sind auch mit Nachteilen behaftet. So lassen
sie z.B. stilistische Möglichkeiten von Programmautoren, die die
Übersichtlichkeit der Programme(-Dokumentation) wesentlich erhöhen
kann, ausser Acht. Ausserdem kann die unüberlegte Anwendung von
SW-Metriken zu Fehlurteilen führen, die grosse Schäden anrichten
können - z.B. wenn herkömmliche SW-Metriken auf objektorientierte
Programme angewendet werden. Weitere Kritikpunkte sind: SW-Metriken haben nur
eine eingeschränkte Aussagekraft, können erst spät eingesetzt
werden und nicht-strukturelle Einflussfaktoren (wie z.B. die
Problemlösungsqualität) können nicht gemessen werden. Aber
häufig lassen sich SW-Metriken automatisch erstellen, sie können
Feedback-Wirkung haben und sind relativ objektiv.
Die Interpretation der Ergebnisse der SW-Metriken hängt ab:
- von der Skalierung des Wertebereichs: Nominal-, Ordinal- oder Kardinal-Skalen.
- Mehrdeutigkeit, wenn verschiedene SW-Metriken die gleiche Ausprägung haben.
Als Beispiel einer sehr einfachen herkömmlichen SW-Metrik
sei hier die Anzahl der Zeilen des Programmcodes (Line of Code, LOC) genannt.
Weitere SW-Metriken stammen z.B. von Halstead und McCabe: das
SW-Science-Mass zur Operanden-/Operatoren-Analyse und die zyklomatischen
Zahlen zur Iterations-/Verzweigungskomplexitätsmessung. Als letztes sei
noch das Informationsfluss-Mass von S. Henry und D. Kafura genannt, wobei
gemessen wird, wie sich Methoden gegenseitig aufrufen und dadurch die
Programmkomplexität bestimmen (lässt sich auch als objektorientiertes
SW-Mass nutzen).
Thema der aktuellen Forschung sind jedoch spezielle
objektorientierte SW-Metriken, denn auf objektorientierte Programme sind
herkömmliche Masse oft nicht anwendbar. Grund: neue Konzepte wie
Klassen und Vererbung werden in herkömmlichen SW-Metriken nicht
berücksichtigt, und der Schwerpunkt bei objektorientierter SW liegt
stärker auf der Schaffung wiederverwendbarer Komponenten. Beurteilt
werden können Klassen (z.B. durch Anzahl Datenelemente), laufzeitbedingte
Objekte (z.B. durch erzeugte Anzahl) und das System (z.B. durch gesamte
Klassenanzahl).
Sehen wir uns ein paar Beispiele für objektorientierte SW-Metriken an:
-
Gewichtete Methode einer Klasse: hier wird jede Methode
einer Klasse mit ihrer statischen Komplexität aufsummiert. So erhält
man mit GMK(K) pro Klasse ein Mass für den Erstellungsaufwand, den
Wartungsaufwand und die Komplexität, die Einfluss v.a. auf
abgeleitete Klassen haben kann.
-
Tiefe einer Vererbungsstruktur (TEV): pro Klasse kann ihre
Höhe im Vererbungsbaum bestimmt werden. TEV(K) ergibt ein Mass
für die Komplexität der Klasse. Ausserdem erfährt man
die Anzahl der Basisklassen, die K beeinflussen können. Aber Achtung! Bei
selektiver oder mehrfacher Vererbung gilt dies nicht in dieser einfachen Form.
Zudem ist die TEV relativ schwer zu bestimmen, wenn Klassenbibliotheken
verwendet wurden, die ihrerseits bereits abgeleitete Klassen enthalten.
-
Anzahl der abgeleiteten Klassen: AAK(K) gibt an, wie viele
Klassen von einer Klasse K direkt abgeleitet sind. Dadurch kann man in etwa
einschätzen, welchen Status die Klasse im System einnimmt: je
grösser AAK(K) ist, desto wichtiger scheint sie zu sein, jedoch
muss der Unterbaum von K diesbezüglich erst noch untersucht
werden.
-
Coupling zwischen Objekten: Anzahl der Beziehungen, die
nicht vererbungsbedingt sind, die ein Objekt zu einem anderen unterhält,
gekennzeichnet durch die Zahl CZO(K). Diese Beziehungen können
Methodenaufrufe, Whole-Part-Strukturen und Objekt-Beziehungen sein. Sie ist wie
bereits erwähnt ein gutes Mass für die Sensibilität
gegenüber Veränderungen im System.
-
Antwortmenge der Klasse K: Menge aller Methoden (eigene
und in Methoden enthaltende), die eine Klasse aufzuweisen hat. Die direkten und
indirekten Methoden werden dabei i.d.R. gleich gewichtet. Die Zahl ADK(K) gibt
dabei an, welchen Einzugsbereich die Klasse innehat; sie entspricht damit dem
Grad der äusseren Verflechtung durch Methodenaufrufe, woraus die
Kommunikationsfreudigkeit und Komplexität einer Klasse ersehen werden
kann.
Die oben aufgezeigten objektorientierten Metriken sind
lediglich Indikatoren für Eigenschaften von Klassen, nicht von ganzen
Programmen. Ihr jeweiliges Mass kann in keiner Weise als anerkannte
Richtlinie geltend gemacht werden, d.h. eine Aussage wie z.B. "GMK(K)>5 ist
schlecht" kann nur subjektiv als WAHR angenommen werden. Dies schränkt
die Aussagekraft der Metriken natürlich erheblich ein. Allerdings, eines
können die Metriken gut: sie verhelfen zu einem relativem Mass, d.h.
man kann damit Klassen eines Programms untereinander vergleichbar machen.
Die Problembereichskomponente ist der Programmteil, der die
eigentliche Problemlösung enthält. Hier werden die OOA-Ergebnisse
umgesetzt, wobei grundsätzlich eine 1:1-Abbildung anzustreben ist, d.h.
der Kern der OOA bleibt stabil, sofern keine Fehler gemacht wurden. Folgende
Punkte, die Thema der nächsten Abschnitte sind, sind beim Design der
Problembereichs-Komponente zu berücksichtigen:
- Einführung abstrakter "Protokollklassen".
- Wiederverwendung früherer Designergebnisse und Klassen.
- Verbesserung des Laufzeitverhaltens.
- Hinzufügen von Klassen für systemnahe Aufgaben.
Eine Protokollklasse wird in C++ durch eine abstrakte
Basisklasse realisiert, d.h. sie besitzt Pure Virtual Functions, die in allen
davon abgeleiteten Klassen benötigt und definiert werden müssen.
Achtung! Protokollklassen tauchen nicht in den Schemata der OOA auf, sondern
erst in den Schemata der OOD.
Beispiel: eine mögliche Protokollklasse für die
Modellierung eines Schachspiels ist die abstrakte Basisklasse "Figur" mit den
rein virtuellen Funktionen "anzeigen", "löschen", "ziehen" und "zug_OK",
und den gemeinsamen Attributen "Feld" und "Farbe". Der Zug einer Figur besteht
darin, den gewünschten Zug zu überprüfen, die Figur zu
löschen, das Attribut "Feld" auf die neue Position zu setzen und die Figur
auf dem neuen Feld anzuzeigen. Zu beachten ist noch, dass die Klasse
"Bauer" ein zusätzliches Attribute-Flag "erster_Zug" benötigt und die
Methode "umwandeln". Die Klasse "Dame" erbt die Methoden und Attribute der
Klassen "Läufer" und "Turm". Und die Klasse "König" benötigt die
zwei Kontrollattribute "rochade_lang_möglich" und "rochade_kurz_möglich",
sowie die zusätzliche Methode "rochieren".
Die Wiederverwendung von Klassen wird v.a. durch die
Verwendung von Klassenbibliotheken realisiert. Doch Klassenbibliotheken sind
mit einigen Problemen verbunden, denn ihre Klassen müssen:
- verwaltet bzw. gespeichert werden, z.B. über eine Datenbank.
- klassifiziert werden, damit sie wiederauffindbar sind.
- dokumentiert werden, am besten mit den OOA- und OOD-Ergebnissen.
- Aspekte wie Copyright, Fehlerhaftung und Bezahlung beachten.
Doch die Vorteile von Klassenbibliotheken überwiegen. So
werden Fehler in den Klassen durch den breiten Einsatz früh bemerkt und
ausgemerzt (Korrektheitssteigerung). Die Klassen werden i.d.R. von
Spezialisten geschrieben (Qualitätssteigerung). Und der Einsatz von
Klassen aus Klassenbibliotheken spart natürlich die Neuentwicklung
(Produktivitätssteigerung).
Wegen des Einsatzes von Klassenbibliotheken, geht man in der
OOD nicht nach der "Top-down"-, sondern nach der "Bottom-up"-Entwurfsmethode
vor: man untersucht die Ergebnisse der OOA, in wieweit sie sich durch bereits
existierende Klassen realisieren lassen. Falls nötig, scheut man dabei
auch nicht, die OOA-Ergebnisse einer adaptiven Bearbeitung zu unterwerfen.
Die Klassenbibliotheken, die man heute beziehen kann (z.B. von
Borlands oder der Universität Mannheim), enthalten i.d.R. relativ
einfache, abstrakte Datenstrukturen wie z.B. Listen, Bäume und Strings,
aber z.T. auch grafische Tools wie z.B. Push-Buttons, Scroll-Bars und
File-Selection-Boxes.
Ein breites Anwendungsfeld besitzen insbesondere
Container-Klassen, die Datenstrukturen wie z.B. Schlangen, Listen, Stacks und
Bäume anbieten, die für nahezu beliebige Datentypen verwendet werden
können. D.h. man kann mit ein und derselben Klasse "Liste" Integer-Zahlen
oder Binary Large Objects verwalten. Die Implementierung lässt sich
auf drei Wegen realisieren: entweder mittels Template-Klassen oder indem Zeiger
auf eine "genormte" Basisklasse (z.B. "Object") verwaltet werden oder indem
void*-Typen im Container verwaltet werden. Die Lösung mittels Templates
ist die beste, denn:
-
Template-Klassen sind typsicher, d.h. "Down-Casts"
(Umwandlung eines Zeigers von der Basis- auf die abgeleitete Klasse) und
Typ-Überprüfungen während der Laufzeit können entfallen.
-
Die vordefinierten Typen (wie z.B. int) können genauso
benutzt werden wie benutzerdefinierte Typen (wie z.B. "sprechende_Enten"). Bei
der Zeiger-Lösung müssten dagegen auch der Standard-Integer-Typ
int von der Basisklasse "Object" abgeleitet werden.
-
Die Performance ist besser, weil kein Overhead durch
virtuelle Elementfunktionen entsteht (die nicht "inline" umgesetzt werden
können).
-
void*Container sind C-typisch, aber nicht C++-typisch: Eine
Laufzeit-Typ-Prüfung in Eigenregie ist (über ein den Typ anzeigendes
static-Datenelement, über eine Index-Klasse oder
Klassennamen-String-Vergleiche) umständlich, und leichter über die
virtuellen Aufrufe der Object-Methode zu realisieren.
Nachteilig an Templates ist, dass sie nicht ohne Weiteres ausgebaut
werden können, und dass der Sourcecode für jeden Typ eines
Template-Containers dupliziert werden muss. Diese Eigenschaften
können den Programmumfang erheblich erweitern und macht ihre Ablegung in
Bibliotheken fragwürdig.
Folgende Punkte können zu einer Verbesserung des
Laufzeitverhaltens von OOP führen:
-
Aufnahme von Datenelementen, die ableitbare Informationen
zwischenspeichern: Darunter versteht man z.B., dass die Anzahl der
Listenelemente nicht jedes Mal langwierig durch Auszählen ermittelt werden
muss, sondern als Datenelement ständig aktualisiert vorliegt.
-
Gezielter Einsatz virtueller Funktionen: Das "late
Binding" durch virtuelle Funktionen kostet Zeit, daher sollte man so viele
Funktionen wie möglich als nicht-virtuell deklarieren - auch wenn sie dann
später nicht so leicht ableitbar sind.
-
Verwendung von "inline"-Funktionen: Funktionsaufrufe
bringen Overhead mit sich, z.B. Stack-Verwaltungsaktionen, die durch
"inline"-Funktionen vermieden werden können - allerdings kommt es dann zu
Code-Duplizierungen, die das Programm verlängern.
-
"Register"-Deklaration: Daten werden nicht im
Hauptspeicher, sondern in den schnellen Registern abgelegt, sofern
möglich.
-
Verwendung von "friends": Über "friend"-Funktionen
kann direkt auf "private"-Elemente von Klassen zugegriffen werden.
-
Referenzenzählen: Statt Objekte z.B. bei der
Parameterübergabe zu kopieren, kann einfach ein Zähler übergeben
werden, der ein Feld einer Referenzliste für Objekte im
selbst verwalteten Speicher adressiert. So kann z.B. Objekt1+Objekt2=Objekt3
dahin gehend realisiert werden, dass Objekt3 Objekt1 einfach
überschreibt, wodurch Speicherplatz gespart wird.
Um die Performance eines Programms auf einem bestimmten
System zu erhöhen, verzichten Programmierer bisweilen auf Standardklassen
und kreieren eigene systemnahe Klassen mit spezifizierten Code und
einsichtigeren Schnittstelle. Dies wird allerdings für den Preis einer
Milderung der Portabilität erkauft. Statt z.B. die Standardprozeduren zur
Erreichung eines "dir"-Aufrufs zu erreichen, kann einfach eine Klasse "dir"
entwickelt werden, die speziell auf das verwendete System zugeschnitten
ist.
Der Entwickler eines Programms muss dafür sorgen,
dass die im Programm verwendeten Daten in den korrekten Zuständen
vorliegen, wozu er sie in bestimmter Weise managen muss. Dies bedarf
besonders bei grossen Projekten einiger Vorbereitung.
-
Externe Datenhaltung: Datenmanagement betrifft eigentlich
nur die Verwaltung von Daten/Objekten ausserhalb eines Programms. Eine
Datenhaltung ausserhalb von Programmen hat folgende Gründe: Die
Datenmenge kann sehr gross sein, die Daten sind lange Zeit verfügbar,
die Daten können von mehreren Programmen verwendet werden und die Daten
können zur IPC genutzt werden.
-
Aspekte externer Datenhaltung: Bei der Benutzung externer
Medien zur Ablegung externer Daten sind folgende "Design"-Aspekte für DBMS
zu berücksichtigen:
-
Die Zugriffsgeschwindigkeit ist langsamer als bei Hauptspeichern.
-
Es sollten Abfragesprachen wie SQL vorhanden sein.
-
Die Daten müssen für das Programm interpretierbar sein.
-
Die Datenkonsistenz ist zu gewährleisten.
-
Grundlagen: Basis für das Datenmanagement bildet ein
konzeptionelles Datenmodell, welches z.B. durch ein ERM oder eine OOA gebildet
werden kann.
-
Objektorientierung und externe Datenhaltung: Die
herkömmliche Datenverwaltung managt nur "passive" Daten, aber OOP
müssen auch "aktive" Objekte mit ihren Methoden verwalten können
(sogenannte persistente Objekte).
-
Besonderheiten: Objekte zu verwalten ist weit schwieriger
als nur Daten zu verwalten. Es müssen Vererbungsstrukturen und Beziehungen
zwischen den Klassen beachtet werden, ausserdem muss
Objektidentität unabhängig von einem Schlüssel gegeben sein
(d.h. der Schlüssel kann sich bei einem Objekt ändern!). Die
Integrität dagegen ist leichter zu erreichen, da nur die Elementfunktionen
die Objekte in definierter Weise ändern können.
-
Die Datenmanagementkomponente: Die externe Datenhaltung
wird bei Coad/Yourdon in der Datenmanagementkomponente modelliert, wobei es hier
besonders auf die Art der externen Speicherung der Objekte ankommt. I.d.R.
werden dazu spezielle Dienste entworfen. Wichtig (und i.d.R. in OODBS
realisiert) ist dabei für das Design der Datenmanagementkomponente:
-
Das Anwendungsprofil: Sollen mehrere Anwender gleichzeitig
die Objekte benutzen können? Dann sind Sperrmechanismen und
Autorisierungsprozesse nötig, und ausserdem Effekte der
Nebenläufigkeit zu beachten.
- Das Zugriffsprofil: Wie wird hauptsächlich zugegriffen (lesend oder schreibend)?
- Die Zugriffsmuster: Sind sie starr oder flexibel?
- Die Datenablegung: Soll sie über DBS oder das Programm erfolgen?
- Die Sicherheit: Müssen sichere Transaktionen garantiert werden
- Die Objekte selbst: Welche müssen persistent sein?
- Die Versionen: Müssen verschiedene Versionen von Klassen verwaltet werden?
- Die Verteilung: Ist eine Verteilung in einem Netz nötig/sinnvoll?
- Die Objektidentität: Ist sie für den Benutzer sichtbar oder nicht?
-
Operationales Anforderungsprofil:
-
Persistenz: Die Abspeicherung wird erreicht durch (a)
spezielle Hilfsklassen zur Benutzung der externen Speicher, durch (b)
klassenabhängige Persistenz, d.h. alle Objekte sind immer persistent, oder
durch (c) objektabhängige Persistenz, d.h. es kann persistente und
nicht-persistente Instanzen einer Klasse geben, was die beste Persistenz
darstellt.
-
Objektidentität: Sie muss (a) Objekte eindeutig
identifizieren (auch wenn sie nicht gleichzeitig existieren), (b) Objekte
dauerhaft identifizierbar machen. Die Identität muss also mit dem
Objekt erzeugt werden und existiert so lange, bis es zerstört wird.
-
Elementare Funktionalität: (a) Abspeichern von
persistenten Objekten, wobei auch alle Objekte, die durch das abgespeicherte
Objekt erreichbar sind, persistent sein müssen, (b) selektives Lesen von
Objekten, von Objektteilen und von ganzen Vererbungsstrukturen bzw. allen
Objekten auf einmal. Sinnvoll ist auch die Funktion (c) Löschen von
Objekten, wobei hier v.a. Konsistenzaspekte zu beachten sind.
-
Folgerungen für das Design: Die Objektidentität
kann mittels eines sogenannten Surrogat-Attributs, welches z.B. den
Erzeugungszeitpunkt festhält, realisiert werden. Klassen, mit einzelnen
persistenten Objekten benötigen ein "Persistenz"-Flag, das angibt, ob eine
Objekt persistent oder transient ist. Von der Einführung von
Modification-Flags muss abgeraten werden, dass dieses von
sämtlichen Klassenmethoden zu berücksichtigen wäre. Sinnvoll ist
eine spezielle Klasse namens "Object_Server", die:
- die Surrogate vergibt.
- die Zugriffsfunktionen für alle Objekte anbietet.
- die Daten/Objekte automatisch abspeichert.
- die unnötige Neueinlesungen eines Objektes verhindert.
Bei der Speicherung von Objekten können mehrere Strategien verfolgt werden:
-
Ein Objekt speichert sich selbst. Während der Laufzeit
entscheidet Objekt, ob eine externe Speicherung nötig ist. Diese von
Coad/Yourdon vorgeschlagene Methode bedarf einer zusätzlichen
Elementfunktion im Objekt (die von "Object_Server" virtuell aufgerufen wird).
Nur hier wird das Datenkapselungsprinzip nicht verletzt.
-
Objekte werden alleine über die "Object_Server"-Klasse
abgespeichert; nur dort ist das Detailwissen dazu nötig.
-
Ein Objekt fordert die "Object_Server"-Klasse auf, es
abzuspeichern. Die Details werden dazu untereinander ausgetauscht.
-
Entwurf der Datenstruktur in relationaler Form mit
Berücksichtigung der Objektstrukturen:
-
Abbildung von Klassen auf Tabellen: Alle Attribute einer
Klasse werden in einer Relation zusammengefasst, z.B. PERSONEN(#Surrogat,
Name, Hobby), oder auf mehrere Tabellen verteilt, z.B. PERSONEN(#Surrogat,
Name) und HOBBIES('Surrogat, Hobby).
-
Abbildung von Beziehungen/Teilobjekten auf Tabellen: Je nach
Kardinalität müssen eigene Assoziationstabellen gebildet werden oder
bestehende um zusätzliche Attribute erweitert werden. Bei eigenen Tabellen
müssen die Surrogate beider Objekte darin aufgenommen werden. Bei
Zusammenfassungen in einer Tabelle dient ein Surrogat als
Sekundärschlüssel. Bei 1:1-Beziehungen lassen sich auch beide Objekte
in nur einer Tabelle darstellen.
Wie bei der Umsetzung von ERM-Strukturen in relationale
Modelle sollen nach Coad/Yourdon auch mit den OOA-Ergebnisse vorgegangen
werden. D.h. es wird bis zur dritten Normalform normalisiert, um Redundanzen zu
minimieren, und dann zur Leistungsverbesserung evtl. wieder bis zu einem
gewissen Grad denormalisiert. I.d.R. führen OODBS sogar gar keine
Normalisierung durch, da dies Nachteile bei Clusterbildung und Stabilität
eines Systems mit sich bringt.
Abbildung der Klassenhierarchien: Hierfür eignen sich die folgenden Methoden:
-
Repeat Class Modell: Objekte werden in jeder Klasse, an der
sie teilnehmen, gespeichert, d.h. in den Basisklassen und in allen beteiligten
abgeleiteten Klassen, wobei die abgeleiteten Klassen die vererbten plus die
eigenen Attribute enthalten.
-
Leaf Overlap Model: Objekte werden nur in den tiefsten
Klasse, zu denen sie gehören, gespeichert, mit ihren Attributen plus den
vererbten Attributen.
-
Split Instance Model: Objekte werden in jeder Klasse, an der
sie teilnehmen, gespeichert, wobei die abgeleiteten Klassen nur ihre Attribute
plus dem Surrogat enthalten.
-
Universal Class Model: Die gesamte Vererbungshierarchie wird
durch eine Klasse repräsentiert. Besitzt ein Objekt eines der abgeleiteten
Attribute nicht, wird es mit NULL gefüllt.
-
Entwurf der Datenstruktur in relationaler Form ohne
Berücksichtigung der Objektstrukturen: Hierbei werden alle Attribute
zusammen mit dem Surrogat in eine binäre Relation gespeichert, wodurch
jegliche Redundanz verloren geht (bis auf die Surrogate-Redundanz!). Leider
gehen dabei alle Strukturinformationen verloren (weil ja keine abgeleiteten
Attribute zusammen in einer Relation stehen können).
-
Entwurf der Datenstruktur in einfachen Dateien: Objekte
lassen sich in Textdateien speichern (Attribut: Attributeausprägungschema)
oder in spezielle Dateien, die neben den Attributen auch noch Hinweise auf die
Strukturen beinhalten, so z.B. die Gesamtlänge, die Attributenamen und die
Länge der einzelnen Attributefelder.
Die ergonomische Gestaltung der Benutzerschnittstelle ist
entscheidend für die Akzeptanz einer SW-Entwicklung. Hierunter fallen
Fenster, Icons, Handbücher, Fehlermeldungen, Hilfssysteme usw.
Insbesondere den grafischen Benutzerschnittstellen (Graphical User Interface, GUI)
werden wir unsere Aufmerksamkeit in den nächsten Abschnitten schenken.
Ein Programm, welches das Geburtsdatum des Anwenders zu wissen
verlangt, aber nirgends vermerkt, dass dies in amerikanischer Schreibweise
einzugeben ist, bevor es zum nächsten Programmpunkt übergeht, wird
dem unwissenden Anwender vermutlich wenig Freude bereiten, denn aus Sicht des
Benutzers verhält sich das System sehr rätselhaft, wenn es
ständig seine Eingaben unbegründet ablehnt.
Benutzerschnittstellen, die Fragen im EDV-Jargon stellen, die
mit Abkürzungen arbeiten, die arbeitsablaufuntypische Antworten verlangen
und die Fehler in Nummernform verkünden, sollten eigentlich längst der
Vergangenheit angehören. Daher muss bereits in der OOA- und OOD-Phase
eng mit dem Anwender zusammengearbeitet werden, und erst wenn der Prototyp mit
der kompletten Benutzerschnittstelle steht, dann sollte mit den
Implementierungsdetails begonnen werden.
Für ergonomische Benutzerschnittstellen benötigt man ein Benutzerprofil, bei dem
spezifische Informationen für den Endanwender gesammelt werden, wie
- Altersprofil: Junge Benutzer sind z.B. meist flexibler.
- Vorkenntnisse: Die Anwender sollen nicht überfordert werden.
- Motivation zur Benutzung des Systems: Beruf, Hobby, ...?
- Die zu erreichenden Ziele des Systems
- Die Persönlichkeit des Endanwenders
Eine gebräuchliche Einteilung der Benutzergruppen ist die
in Anfänger, Fortgeschrittene und Experten, wobei die erste Gruppe
Hilfssysteme benötigt und die letzte Gruppe schnelle Antwortzeiten
verlangt. Besonders wenn eine Schnittstelle für mehrere Gruppen Geltung
haben soll, erschwert dies das Design. Ein System kann angepasst,
anpassbar oder anpassungsfähig an seine Benutzer sein.
-
Dialogformen:
-
Benutzergeführter Dialog: Benutzer agiert, System
reagiert; z.B. Kommandosprachen.
-
Systemgeführter Dialog: System agiert, Benutzer
reagiert; z.B. Menüs.
-
Gemischter Dialog: System/Benutzer agieren/reagieren
abwechselnd; z.B. direkte Manipulation.
-
Dialogarten:
- Menüs: langsam, aber Eingabeaufwand minimal.
- Eingabemasken: Tools einsetzbar, aber benötigt viel Bildschirmplatz
- Kommandosprachen: mächtig, aber komplex; z.B. UNIX
- Natürliche Sprache
- Direkte Manipulation: visualisiert, aber schwer zu programmieren; z.B. Maus
-
Die "acht goldenen Regeln" des Dialogdesigns:
- Bemühung um Konsistenz: Einheitlichkeit der Symbole/Bezeichnungen
- Abkürzungen ermöglichen: z.B. Macros
- Feedback anzeigen nach Aktionen
- Zusammenhängende Aktionen gruppieren
- Einfache Fehlerbehandlung: Schwere Fehler ausschliessen
- UNDO-Funktion<(LI>
- Interne Kontrolle
- Hilfssysteme anbieten
-
Fehlervermeidung: Selbst erfahrene Anwender bedienen
Systeme fehlerhaft. Daher ist es wichtig, eine ansprechende Fehlerbehandlung
durchzuführen. So sollen z.B. nicht nur Fehlernummer ausgegeben werden
oder Texte der Form "Syntax Error", sondern präzise Fehlerbeschreibungen
wie "Linke Klammer nicht geschlossen". Auch hier ist auf Konsistenz zu achten.
Fehler können auf drei Arten vermieden werden, indem garantiert wird:
- Korrekte Klammerebenen: z.B. durch automatisches Einrücken
- Vollständige Befehlsfolgen: z.B. durch Macros
- Korrekte Befehle: z.B. durch Kommandoauswahl durch Anklicken
-
Anzeige und Eingabe von Daten: Die Eingabe von Daten
sollte dem Anwender so einfach wie möglich gemacht werden. Ableitbare
Informationen sollte das System selbst stellen und auch bestimmte Eingaben
vorgeben oder auswählbar gestalten. Der Anwender sollte sich keine
längeren Eingaben merken müssen. Und die Texte werden so angezeigt,
wie sie eingegeben wurden.
- Online-Handbücher: z.B. "man"-System von UNIX oder CD-ROM-Handbücher.
- Funktionstasten- und Kommandoübersichten: Für erfahrene Anwender.
- Tutorials, Demos etc.
-
Hilfssysteme: Hilfssysteme können integriert in das
System oder zusätzlich dabei sein, sie könne aktiv sein (laufende
Hilfeanzeige) oder inaktiv, und sie können spezifiziert Anfragen zum
Aufruf benötigen oder kontextsensitiv, d.h. je nach dem aktuellen
Programmpunkt, erscheinen. Üblicherweise sind Hilfssysteme in Form eines
Albums oder vernetzt strukturiert, d.h. eine Information weist auf eine andere.
Beispiel: Das Borland C++-Hilfssystem.
-
Menüarten:
- Menümasken: Das Menü bedeckt den gesamten Bildschirm.
- Pop-Up-Menüs: Das Menü erscheint als Fenster bei der Mausposition.
- Pull-Down-Menüs: Das Menü wird von Menüleiste "herunter gezogen".
- Stichwort-Menüs: Meistens am unteren Rand in der Form "1=Drucken".
-
Menüstrukturen:
- Einzelmenü: z.B. ein Fenster "END" mit den Buttons "CANCEL"/"OK".
- Lineare Sequenzen: Entweder-Oder-Feldern, z.B. "digital" "analog".
- Baumstruktur: Menüs, die jeweils Nebenmenüs anzeigen.
Das Seeheim-Modell ist ein Architekturmodell für User
Interface Management Systeme (UIMS) von grafischen, interaktiven Systemen. Es
unterteilt Programme in die folgenden vier Komponenten:
- Präsentationskomponente: Oberflächendarstellung für Benutzer
- Dialogsteuerung: Eingabe-/Ausgabe-Interpretation
- Anwendungsschnittstelle
- Anwendungskomponente: Enthält eigentlichen Anwendungsobjekte.
Ein User Interface Management System (UIMS; wie z.B. DevGuide von SUN) trennt
die Applikationen (die Anwendungskomponenten), die z.B. über 4GL-Tools erstellt
wurden, von der Benutzerschnittstelle. Die Präsentationskomponente wird dabei durch
eine deskriptive Beschreibungssprache oder ein grafisch-interaktives Tools
generiert. Die Spezifikation wird interpretativ abgearbeitet. Es sind mehrere
verschiedene Schnittstellen zu einer Applikation generierbar, was besonders im
Rahmen des Prototyping von Vorteil ist. Probleme:
- Die Oberflächenobjekte müssen auf die Applikationsobjekte passen.
- Die Spezifikationssprachen müssen vom Entwickler erst erlernt werden.
- Interpretative Spezifikationssprachen gehen mit Effizienzverlust einher.
- Nur durch die Spezifikationssprachen-Erweiterung ist eine UIMS-Erweiterung möglich.
Anwendungsrahmen (Application Frameworks) sind erweiterte
UIMS, denn neben Oberflächenobjekten können hier auch generische
Klassen für die eigentliche Applikationsentwicklung genutzt werden. Der
Schwerpunkt der Framework-Entwicklung liegt dann weniger auf den Objekten
selbst, sondern stärker auf den Beziehungen zwischen den Objekten. Klassen
werden wiederverwendet und an die Applikation angepasst, indem man
abgeleitete Klassen generiert. Wird etwas an den Klassen der
Framework-Bibliothek geändert, so wirkt sich dies natürlich auch auf
alle abgeleiteten Klassen der Applikationen aus. Ein Beispiel für einen
Anwendungsrahmen: XVT++ mit Klassenstrukturen wie z.B. XVT_Base abgeleitet
XVT_Container abgeleitet XVT_Dialog oder Standalone-Klassen wie z.B. XVT_Brush,
XVT_Menu und XVT_Timer.
Als Prozess wollen wir die Instanz eines Programms
bezeichnen, das vom Betriebssystem ausgeführt wird. Er hat folgende
Eigenschaften:
-
Er besteht aus zeitlich einander nicht überlappender Schritte.
-
Er hat eine zeitlich begrenzte Lebensdauer.
-
Er hat i.d.R. eine disjunkte (Speicherplatz-)Umgebung.
-
Er kann zu Gunsten eines anderen Prozesses unterbrochen werden.
Bei einem Multiuser-Betriebssystem wie UNIX z.B., wird mit dem
Einloggen ein Prozess geboren, der den Benutzer durch eine Shell zur
schrittweisen Weitereingabe auffordert, wobei jede Eingabe einen neuen
Prozess mit eigenem Stack und Heap generiert, der den Login-Prozess
unterbricht, bis man schliesslich die UNIX-Sitzung beendet und den
Login-Prozess wieder sterben lässt.
Alle Prozesse in UNIX werden mit dem Systemcall fork()
erzeugt. Der Vater-Prozess, der fork() aufruft, wird dabei mit seiner
Umgebung in einen neuen Speicherbereich kopiert, in dem er als relativ
eigenständiger Sohn-Prozess weiter läuft. Durch die Kopie werden
dem Sohn-Prozess auch alle offenen File-Deskriptoren des Vater-Prozesses
vererbt - dies ist in sofern etwas besonderes, da File-Deskriptoren sonst nicht
zwischen zwei Prozessen ausgetauscht werden können. Wie der
Vater-Prozess, so erhält auch der Sohn-Prozess eine eindeutige
Prozessidentifikationsnummer (PID) vom Betriebssystem zugewiesen. Zu
beachten ist noch, dass der fork()-Systemcall zwar einmal aufgerufen, aber
zweimal beendet wird, nämlich vom Sohn- und vom Vater-Prozess.
Einziger Unterschied: Der Vater-Prozess liefert die Sohn-PID zurück
und der Sohn-Prozess eine NULL. Abgesehen von der PID ist dieser
Rückgabewert die einzige Möglichkeit, den Vater- vom Sohn-Prozess
zu unterscheiden; alle anderen Variablen und die Verarbeitungsposition sind
identisch.
Nach der Prozessgenerierung laufen Vater und Sohn
asynchron zueinander ab. Durch den wait()-Aufruf kann ein Vater-Prozess auf
die Beendigung des Sohn-Prozesses warten. Der Sohn-Prozess - noch eine
identische Kopie des Vater-Prozesses - ruft i.d.R. den exec()-Systemcall auf, um
ein eigenes Programm ausführen zu können, wozu er neue Segmente
(Stack-, Daten- und Textsegment) benötigt. Der Systemaufruf exit() beendet
den Sohn-Prozess und übergibt ein Statuswort an den (wartenden)
Vater-Prozess.
Die eben ausgeführten Beschreibungen beziehen sich auf
sogenannte Heavyweight-Prozesse, die bei UNIX üblich sind. Später
werden wir uns aber auch noch den Lightweight-Prozessen zuwenden.
Coad und Yourdon schlagen zum Design der
Task-Managementkomponente vor, zunächst mögliche Tasks zu
identifizieren, diese in ereignisabhängige und zeitabhängige Tasks zu
klassifizieren, dann die Prioritäten der einzelnen Tasks festzulegen und
abzuwägen, ob ein Task-Koordinator (Scheduler) benötigt wird.
Schliesslich sind alle Tasks nach einer vorgegebenen Schablone zu
beschreiben.
-
Identifizieren ereignisabhängiger Tasks: Ein
ereignisabhängiger Task wartet bei minimalen Verbrauch von
Systemressourcen auf ein Ereignis, z.B. auf das Ende einer Tastatureingabe, und
aktiviert sich bei Eintritt desselben. Danach terminiert er oder geht wieder in
den Wartezustand über. Z.B. wartet ein Prozess durch den folgenden
Systemcall auf ein Signal-Ereignis: signal(SIG_INT, Sig_Handler);
-
Identifizieren zeitabhängiger Tasks: Ein
zeitabhängiger Task wartet bei minimalen Verbrauch von Systemressourcen
auf den Zeitpunkt seiner nächsten Aktivität, z.B. auf die Bestimmung
der Mausposition, und aktiviert sich bei Eintritt derselben. Danach terminiert
er oder geht wieder in den Wartezustand über. Z.B. wartet ein Prozess
durch den folgenden Systemcall ein paar Sekunden: sleep(Sleep_Sek);
-
Identifizieren von Task-Prioritäten:
Terminiert/deaktiviert sich ein Task, dann wird der Task mit der
nächsthöheren Priorität gestartet. Als kritische Task werden
diejenigen Tasks bezeichnet, die eine hohe Prioritäten benötigen, um
die Gesamtfunktionalität des Systems zu gewährleisten.
-
Identifizieren eines Task-Koordinators: Bei mehr als zwei
Tasks ist die Einrichtung eines Scheduler zu erwägen, besonders wenn Tasks
unterschiedlicher Prioritäten koordiniert werden müssen. Der
Task-Koordinator legt dabei nur fest, in welcher Reihenfolge wartende Tasks
gestartet werden. Problembezogene Methodenaufrufe sind nicht seine Aufgabe.
Die identifizierten Tasks müssen nach folgenden Aspekten
beschrieben werden:
- Was wird vom Task geleistet? Name, Methoden, Beschreibung
- Wie wird der Task koordiniert? Ereignisabhängig oder zeitabhängig?
- Wie kommuniziert der Task? IPC-Beschreibung
Folgende Schablone bietet sich zur Beschreibung an:
Task j:
Name: Temperaturpruefer
Beschreibung: Verantwortlich fuer Temperaturueberwachung
Methoden: Thermometer.GibTemperatur()
Prioritaet: Mittel
Koordinierung: Zeitabhaengig, alle 100 ms aktiv
Kommunikation: Erhaelt Informationen vom Thermometer
Liefert Informationen an Erfassungsdatei
Die Tasks von System V arbeiten mit Lightweight-Prozessen (LWP). Im Gegensatz
zu den Heavyweight-Prozessen verfügen die LWP über keinen eigenen Adressraum,
sondern nur über einen eigenen Stack. Die NIHCL (National Institute of Health
Class Library) unterstützt dieses moderne Konzept durch die Klassen
"Process", "Scheduler", "Semaphore" und "Shared_Queue".
-
Tasks und Klassen der NIHCL: Alle NIH-Tasks sind Objekte
von Klassen, die von der abstrakten Klasse "Process" abgeleitet wurden. Diese
Tasks werden alle von einem Objekt der Klasse "Scheduler" koordiniert. Um
Signale zwischen den Tasks auszutauschen, muss ein Objekt der Klasse
"Semaphore" benutzt werden. Und mithilfe einer Instanz der Klasse
"Shared_Queue" können sogar ganze Objekte zwischen Tasks ausgetauscht
werden.
Die LWP der NIHCL sind mit folgender Charakteristika realisiert:
-
Innerhalb eines Programms sind beliebig viele Tasks
erzeugbar. Die "Prozess"-Instanzen benutzen dabei jeweils einen eigenen Stack,
aber denselben Datenteil und dieselben Datei-Deskriptoren.
-
Jeder Task verwaltet seinen eigenen Stack. Dadurch sind sie
rekursiv abzuarbeiten und verfügen über eigene lokale Variablen.
-
Jeder Task befindet sich im Zustand SUSPENDED, RUNNING oder
TERMINATED. Nach der Erzeugung ist ein Task im Zustand RUNNING und kann vom
Scheduler aktiviert werden. Es gilt: Es ist immer nur ein Task aktiv. Wird ein
Task deaktiviert, dann kann er in den SUSPENDED-Zustand übergehen und mit
Hilfe des resume()-Systemcalls durch einen anderen (aktiven) Task wieder
RUNNING gesetzt werden. Oder er geht in den TERMINATED-Zustand über, der
nicht mehr verlassen werden kann.
-
Ein Task heisst aktiv, wenn er die CPU benutzt. Es kann
immer nur genau ein Task zu einem Zeitpunkt aktiv sein. Die Aktivierung der
RUNNING-Tasks wird vom Scheduler nach dem First-Come-First-Serve-Prinzip und
unter Beachtung der Prioritäten (0-7) vorgenommen. Für jede
Priorität führt der Scheduler eine eigene "run_List" und für
SUSPENDED-Tasks eine "wait_List".
-
Aktive Tasks können vom Scheduler nicht unterbrochen
werden (im Gegensatz zu Heavyweight-Prozessen). Ein Task muss sich mittels
suspend()- oder terminate()-Aufrufe selbst deaktivieren oder anderen Tasks
über einen yield()-Aufruf die Möglichkeit geben, aktiv zu werden.
-
Mittels der "Semaphore"-Klasse ist bes möglich, Tasks
im SUSPENDED-Zustand auf Ereignisse warten zu lassen, z.B. externe UNIX-Signale
oder einen bestimmten Semaphoren-Wert.
-
Die Klassen "Process", "Stack_Proc" und "Heap_Proc": Von der
abstrakten "Process"-Klasse sind die Klassen "Stack_Proc" und "Heap_Proc"
abgeleitet, von denen wiederum die konkreten Task-Klassen abgeleitet werden.
Normalerweise werden Tasks auf den effizienten Heap gelegt, doch durch die
Verwendung der "Stack_Proc"-Klasse kann man sie auf den Stack umkopieren, wo sie
leichter auf Fehler hin zu untersuchen sind. Ein Task-Klasse erbt dabei folgende
Eigenschaften:
- Den Zustand (SUSPENDED; RUNNING, TERMINATED).
- Die Elementfunktion Process::suspend()
- Die Elementfunktion Process::terminate()
- Die Elementfunktion Process::resume()
Standardvorgehensweise:
class Test_Task:public Heap_Proc {
public:
static void create(int priority) {
stack_Typ t1;
new Test_Task(&t1, priority);
};
Test_Task(stack_Typ *t1, int priority) : HeapProc("test", t1, priority); {};
};
-
Eine Schablone für den Task-Konstruktor:
Standard-Implementation:
Test_Task::Test_Task(stack_Typ *t1,int priority):Heap_Proc("test",t1,priority);
{
if(FORK()!=0) {
Scheduler::yield(); // starte Sohn, bevor selber fortfahren
return;
};
// Sohn-Task-Anweisungen
terminate();
};
Der Task-Konstruktor hat dabei folgendes zu leisten: Er
muss FORK() aufrufen, um einen neuen LWP zu erzeugen. FORK() ist eine
Elementfunktion von "Heap_Proc"/"Stack_Proc" und arbeitet ähnlich wie
UNIX-fork(). Er muss ausserdem die Funktionalität des
Sohn-Prozesses für FORK()==0 enthalten, und er muss den
Sohn-Prozess mit terminate() beenden.
-
Task-Koordination mit der Klasse "Scheduler": Jedes
C++-Programm kann maximal einen Task-Koordinator erzeugen. Dazu wird die
globale Funktion MAIN_PROCESS(int priority) aufgerufen. Der gerade aktive Task wird
mit einem static-Datenelement gespeichert:
Process *Scheduler::active_process;
Verwaltet werden eine "run_List"-Struktur pro Priorität
und eine "wait_List"-Struktur. Über einen terminate()-Aufruf wird ein Task
aus allen Listen entfernt. Jeder Task kann die Koordinierung durch den
Scheduler durch einen einfachen Scheduler::schedule()-Aufruf auslösen. So
wie er an der Reihe wird, wird er aktiviert und muss sich dann selbst
deaktivieren. Über yield() kann ein Task auch Tasks mit bestimmter oder
höherer Priorität aktivieren - ist kein solcher in der "run_List",
dann fährt er selbst fort. Und auch über den wait()-Aufruf kann sich
ein Task deaktivieren; in diesem Falle wartet er auf ein Semaphore-Ereignis.
Die letzte Möglichkeit zur Deaktivierung besteht darin, dass sich
der Task über den Aufruf Process::select() auf ein
File-Deskriptor-Ereignis wartet.
-
Message Passing mit der Klasse Shared_Queue: Tasks
können Nachrichten als "Object"-Objekte über Instanzen der Klasse
"Shared_Queue" austauschen. Dabei fügen Tasks Nachrichten an das Ende der
Queue mit nextput(Object&) an oder entnehmen sie mit next() am Anfang. Will
ein Task eine Nachricht in eine volle Queue eingeben, dann wird er suspendiert,
bis dies möglich ist. Zur Entnahme wird die "Semaphore"-Klasse
benötigt. Auch ein entnehmender Task kann suspendiert werden, falls die
Queue leer sein sollte.
-
Task-Synchronisation mit der Klasse Semaphore: Mit
Objekten der Klasse "Semaphore" werden Tasks synchronisiert. Dazu wird eine
"wait_List" geführt, die alle Tasks speichert, die SUSPENDED sind und auf
ein Semaphore-Ereignis warten. Dieses Ereignis wird allerdings nicht von der
Semaphore, sondern von anderen Tasks generiert. Eine Count-Variable
inkrementiert, wenn ein Signal bei einem Semaphor ankommt und dekrementiert,
wenn ein Task wait() aufruft und in die "wait_List" aufgenommen wird. Bei
negativem Count müssen Tasks warten, bei positivem Count können sie
sofort fortfahren. Ruft ein Task für ein Semaphore-Objekt signal() auf,
dann wird dessen Count inkrementiert.