Im Anfang schuf Gott Himmel und Erde; die Erde aber war wüst und wirr, Finsternis lag über der Urflut, und Gottes Geist schwebte über dem Wasser. Gott sprach: Es werde Licht. Und es ward Licht.
Die Bibel, Erstes Buch Mose, 1, 1-3
In der Genesis ist die Schaffung einer Ordnung aus dem Tohuwabohu der erste Schritt der Schöpfung. Die Ordnung, die Struktur, ist die Voraussetzung für alle weiteren Schritte. Vor der Ordnung gibt es nur den Geist und das Chaos. Die in der Schöpfung entstandenen Strukturen wie Tag und Nacht oder Himmel und Erde sind absolut stabil. Damit wird – ganz unabhängig vom religiösen Gehalt – die überragende Bedeutung der Strukturierung deutlich. Wer ein komplexes System schafft, legt Strukturen fest, die lange Zeit gültig bleiben. Darum gilt das Entwerfen auch in der Software-Entwicklung als die zentrale Tätigkeit. Fehler im Entwurf bleiben uns oft über Jahrzehnte erhalten.
Gerade von einem Kapitel über den Entwurf kann man einen ordentlichen Entwurf erwarten, also eine leicht verständliche, übersichtliche und tragfähige Struktur. Leider widersetzt sich dieses Thema jedoch einer eleganten Strukturierung. Hier nützt – wie auch an vielen anderen Stellen dieses Buches – der Blick hinüber zu den Architekten: Ein Haus kann man aus Lehm, Holz, Stein oder vielen anderen Materialien bauen, man kann es flach oder hoch, rund oder eckig machen. Diese Möglichkeiten lassen sich nicht durch eine geschlossene Entwurfslehre fassen. Das Gleiche gilt beim Software-Entwurf: Je nach Art der zu entwickelnden Software und je nach der gewählten Entwicklungsstrategie spielen ganz unterschiedliche Überlegungen eine Rolle.
Nach einigen Betrachtungen zur Bedeutung des Entwurfs im Entwicklungsprozess (Abschnitt 17.1) führen wir Begriffe ein, die im Kontext des Software-Entwurfs wichtig sind (Abschnitt 17.2). Im Abschnitt 17.3 präsentieren wir allgemeingültige Prinzipien des Entwurfs. Anschließend behandeln wir Aspekte des objektorientierten Entwurfs (Abschnitt 17.4). Mit der Wiederverwendung von Architekturen und architektonischem Wissen befassen wir uns im Abschnitt 17.5. Eine kurze Betrachtung über die Beurteilung der Entwurfsqualität (Abschnitt 17.6) schließt das Kapitel ab.
Wenn ein Software-Entwickler eine sehr große, komplexe Software realisieren soll, steht er zunächst vor dem Problem, dass er die vielen Facetten des Systems und ihre Wechselwirkungen nicht überblicken kann; er muss die Software also in seiner Vorstellung gliedern. Wenn damit überschaubare Einheiten entstanden sind, muss er zweckmäßige Lösungsstrukturen festlegen. Schließlich muss er die sehr zahlreichen Bestandteile seiner Software so organisieren, dass er den Überblick behält.
Mit dem Entwurf verfolgen wir also drei Ziele, die sich nicht scharf gegeneinander abgrenzen lassen:
1. Gliederung des Systems in überschaubare (handhabbare) Einheiten
2. Festlegung der Lösungsstruktur
3. Hierarchische Gliederung
Als Software-Architektur (siehe Definition in Abschnitt 17.2.4) bezeichnen wir vor allem das Resultat der Gliederung (Schritt 1), die eine Lösungsstruktur auf höchster Ebene festlegt (Schritt 2).
Man vergleiche unsere Situation mit der Aufgabe, ein großes Bauwerk an einen anderen Standort zu versetzen. Dazu muss man das Bauwerk zumindest so weit zerlegen, dass die einzelnen Teile handhabbar werden. Was das konkret bedeutet, hängt natürlich auch von der Ausrüstung (Kran, Fahrzeuge) und von den eigenen Fähigkeiten und Erfahrungen ab. Beim Entwurf ist es ganz ähnlich.
Es ist also nicht zu erwarten, dass die Spezifikation zu Beginn der Entwicklung vollständig vorliegt und der Entwickler sie auch vollständig kennt und versteht, bevor er einen Entwurf anfertigt. Vielmehr verhilft oft erst der Entwurf zum notwendigen Verständnis. Daher arbeiten erfahrene Entwickler mit Standardstrukturen (Abschnitt 17.5.1), die sie für eine große Klasse von Aufgaben einsetzen. Die Wiederverwendung vorhandener Komponenten hat eine ähnliche Wirkung wie die Verwendung einer Standardstruktur.
Die Struktur eines Gegenstands ist die Menge der Beziehungen zwischen seinen Teilen; ein Gegenstand, der nicht aus (erkennbaren) Teilen aufgebaut ist, heißt unstrukturiert oder amorph. Die Struktur ist also ein Aspekt des Gegenstandes, der von den materiellen und quantitativen Aspekten abstrahiert.
Die bemerkenswerteste Eigenschaft von Strukturen ist ihre oft überraschende Stabilität. Viele Strukturen des täglichen Lebens überdauern die Materie, an die sie gebunden scheinen. So zeigen viele Fossilien nur noch die Gestalt, die Struktur der Organismen, deren Atome in Jahrmillionen vollständig ausgetauscht wurden. Ebenso kennen wir in der Gesellschaft viele stabile Strukturen, z. B. Dörfer, Vereine, Gesetzeswerke, Spielregeln, die sich über Jahrhunderte erhalten, ohne dass man auf irgendein Detail zeigen könnte, das unverändert geblieben ist: Das Konkrete ist vergänglich, das Abstrakte ist stabil.
Das gilt auch für Software. Wenn in einem großen System immer wieder Komponenten verändert oder ausgetauscht werden, ist nach einigen Jahren kaum noch eine Zeile aus dem Code des ursprünglichen Systems erhalten, aber die Struktur ist völlig unverändert. Da die Software-Struktur großen Einfluss auf die Brauchbarkeit (Effektivität), vor allem aber auf die Wartbarkeit hat, ist die Wahl einer sinnvollen Struktur die wichtigste (technische) Entscheidung der ganzen Software-Entwicklung.
Ein Mensch kann nur Systeme überblicken, die aus wenigen Teilen bestehen; die Psychologie gibt dazu die Zahl sieben an (Miller, 1956), d. h., bis zu (etwa) sieben Gegenstände können ohne Weiteres erfasst werden. Sind es mehr, so muss man die Gegenstände nacheinander betrachten und ihren Zusammenhang systematisch entwickeln. Betrachtet man Deklarationen und Anweisungen als Bestandteile eines Programms, dann bestehen schon kleinere Software-Systeme aus Tausenden dieser Komponenten. Sie müssen gruppiert, also hierarchisch organisiert werden, um für uns überschaubar zu sein.
Simon (1962) zeigt mit seiner Uhrmacher-Analogie, dass uns nur die Abstraktion ermöglicht, sehr komplexe Objekte herzustellen. Von zwei gleich kompetenten Uhrmachern, die beide ihre Uhren aus jeweils hundert Teilen zusammensetzen, wird einer sehr erfolgreich, während der andere scheitert. Der Erfolgreiche hat die Teile jeweils in Baugruppen zu zehn Teilen vormontiert, die am Schluss zusammengefügt werden; der andere setzt in einem einzigen Schritt alle hundert Teile zusammen. Stört ein Kunde, der eine Uhr bestellen oder abholen will, die Montage, so verliert der eine die Arbeit von wenigen Minuten, der andere von Stunden, und es gelingt ihm schließlich nicht mehr, auch nur eine einzige Uhr fertigzustellen.
Darum kann man nur kleine Programme direkt auf der Grundlage der Anforderungen codieren; alle anderen müssen schrittweise entwickelt werden.
Da man bei der Vorbereitung eines Hausbaus ähnlich vorgeht, bietet sich die Architektur-Metapher an. So, wie die Architektur eines Gebäudes die Formen und Beziehungen der Hauptbestandteile, also der Wände, Decken, Dachflächen, Türen und Fenster, vorgibt oder beschreibt, so macht die Software-Architektur Aussagen über die Gestalt, die Funktionen und Beziehungen der Software-Komponenten.
Durch die sinnvolle Gliederung (und nur durch sie) entsteht die Möglichkeit zur Abstraktion.
Ist die Verwendung einer Standardarchitektur nicht möglich oder nicht sinnvoll, muss der Entwurf sukzessive entstehen. Dabei kann man vier verschiedene Vorgehensrichtungen unterscheiden, die mit den Schlagwörtern top-down, bottom-up, outside-in und inside-out bezeichnet werden. Die mit dieser Wortwahl verbundene räumliche Vorstellung ist in Abbildung 17–1 dargestellt.
Abb. 17–1 Vorgehensrichtungen beim Software-Entwurf
Geht man von der (abstrakten) Aufgabe zur konkreten, detaillierten Lösung des Problems, so bezeichnet man das als Top-down-Entwicklung. Dabei zerlegt man die Aufgabe rekursiv in Teilaufgaben, bis die elementare Ebene (der Befehlsvorrat der Programmiersprache und die Betriebssystemaufrufe) erreicht ist. Man bezeichnet dieses Verfahren nach Niklaus Wirth (1971) auch als Schrittweise Verfeinerung. Wenn auf der Ebene der Implementierung keine ungewöhnlichen Probleme zu erwarten sind, ist die Top-down-Entwicklung der übliche Ansatz.
Diesem analytischen Vorgehen steht die Entwicklung bottom-up gegenüber, bei der der Entwickler die Befehle so lange synthetisiert (kombiniert), bis eine Gesamtlösung entstanden ist. Wer auf einer exotischen Hardware implementiert, wird wahrscheinlich bottom-up beginnen, also zunächst den virtuellen Rechner realisieren, der die benötigten Operationen anbietet.
Geht man von der Bedienoberfläche aus in Richtung auf die Datenstrukturen und Algorithmen vor, so erfolgt die Entwicklung outside-in. Das ist sinnvoll, wenn die Schnittstelle feststeht, die Implementierung aber offen ist. Sehr oft entsteht diese Situation, wenn die Kunden bereits mit einem Prototyp der Bedienoberfläche einverstanden waren.
Beginnt man umgekehrt bei den »Innereien« des Systems und arbeitet von dort zur Bedienoberfläche hin, so handelt es sich um eine Inside-out-Entwicklung. Sie ist typisch für eine Situation, in der ein bestehendes (Informations-)System um eine neue Funktion erweitert wird, z. B. eine (nur vage spezifizierte) grafische Ausgabe.
In der Praxis lassen sich die Ansätze nicht in Reinform anwenden. Um den Weg zur Lösung zu finden, muss man in jedem Falle beides kennen, den Ausgangspunkt und den Zielpunkt. Die zweckmäßige Vorgehensrichtung hängt von den konkreten Randbedingungen ab. Wenn bestimmte Punkte hart vorgegeben sind, etwa die Datenbank in einem bestehenden System oder eine präzise Spezifikation der Funktionalität, dann ist es zweckmäßig, von diesem Fixpunkt auszugehen, sich also vom Vorgegebenen zum Gestaltbaren hin zu bewegen. Da Entwickler in der Regel mit der Programmiersprache und dem Betriebssystem souverän umgehen können, ist diese Seite für sie gestaltbar, sie arbeiten also top-down.
In der Praxis wird dem Entwurf nicht immer die Aufmerksamkeit zuteil, die seiner Bedeutung angemessen wäre. Allgemeine, den Entwicklern bekannte Grundsätze für die Gestaltung der Systeme werden als ausreichende Festlegung der Architektur betrachtet, ein Entwurf findet erst auf Detailebene statt. Die Architektur wird in dieser Situation natürlich auch nur unzureichend dokumentiert.
Sie sollte aber dauerhaft und prüfbar dokumentiert werden, denn sonst ist es unmöglich, sie zu diskutieren und zu prüfen und so das Risiko einer schlechten Architektur zu verringern. Außerdem kann nur eine dokumentierte Architektur von mehreren Personen parallel implementiert werden. Ob für die Beschreibung eine wohldefinierte Sprache eingesetzt wird oder eine frei gewählte Notation, typischerweise aus natürlicher Sprache und einfachen Grafiken, hängt vom Projekt und von den Fähigkeiten und Möglichkeiten der Beteiligten ab.
Im Laufe der Wartung besteht die Gefahr, dass die Architektur durch eine Vielzahl kleiner Modifikationen schleichend korrumpiert wird und dadurch nicht nur Klarheit und Verständlichkeit einbüßt, sondern sich auch von ihrer Beschreibung entfernt, in der die Änderungen meist nicht nachgeführt werden. Damit verliert die Architekturbeschreibung ihren Wert für die Wartung der Software.
Northcote Parkinson1 hat festgestellt, dass der Aufwand für Entscheidungen in Organisationen keineswegs proportional dem Risiko ist, sondern oft eher reziprok: Details werden von Fachleuten ausführlich erörtert und systematisch geklärt, aber die wirklich wichtigen Fragen werden nicht diskutiert, weil die einen nichts davon verstehen, die anderen keine Hoffnung haben, die Probleme verständlich zu machen; darum wird die Vorlage einfach abgenickt, auch wenn es gute Gründe gibt, sie abzulehnen. Ähnliches gilt auch für den Architekturentwurf. Obwohl seine Bedeutung allgemein anerkannt ist, wird in der Praxis sehr oft eher zufällig irgendeine Struktur ungeprüft akzeptiert oder von einem Vorgängersystem übernommen und anschließend in Code gegossen.
Dies ist insbesondere deshalb problematisch, weil sich die statische Struktur später nur noch mit hohem, meist unangemessenem Aufwand verändern lässt. Was als romanische Basilika gebaut wurde, wird niemals zu einer freitragenden Messehalle. Die Systemfunktion stellt die Anforderungen an die statische Struktur. Die Funktion kann aber später trotzdem meist relativ leicht verändert werden.
Im Bauwesen kann man auf viele wohlverstandene Begriffe wie »Tür« und »Dach« zurückgreifen, im Software Engineering fehlen uns nur allzu oft klare Begriffe. Die Literatur bietet eine Reihe von Definitionen an, die aber kein konsistentes System ergeben. Wir müssen also auswählen, was zusammenpasst, oder selbst definieren, was wir brauchen.
Informatiker weichen gern auf das Wort »System« aus, wenn sie keinen treffenderen Ausdruck finden. Die meisten Definitionen, die für den Systembegriff angegeben werden, führen über verwandte Begriffe wie Konfiguration und Komponente wieder zum System zurück, sind also zyklisch. Wir müssen uns anscheinend damit abfinden, dass wir über den intuitiven Systembegriff nicht hinauskommen, so wie er beispielsweise im IEEE-Standard 1471 (2000) eingeführt wird:
system — A collection of components organized to accomplish a specific function or set of functions.
IEEE Std 1471 (2000)
Leider ist der Systembegriff nicht sehr aussagekräftig, ein System besteht typischerweise – aber nicht zwangsläufig – aus mehreren miteinander vernetzten Komponenten, es ist (physisch oder logisch) von seiner Umgebung unterscheidbar, und es gibt einen – meist funktionalen – Aspekt, unter dem das System eine Einheit darstellt.
Ein Software-System ist demnach ein System, dessen Komponenten Software sind. Genauer gesagt sind die Komponenten einzelne Software-Einheiten oder bestehen aus Software-Einheiten. Wir können also definieren:
Ein Software-System ist eine Menge von Software-Einheiten und ihren Beziehungen, wenn sie gemeinsam einem bestimmten Zweck dienen. Dieser Zweck ist im Allgemeinen komplex, er schließt neben der Bereitstellung eines ausführbaren Programms (oder auch mehrerer) gewöhnlich die Organisation, Verwendung, Erhaltung und Weiterentwicklung ein.
Die Software-Einheiten werden im Kontext der Konfigurationsverwaltung definiert (siehe Abschnitt 21.1.1).
Der Systembegriff stützt sich auf den Komponentenbegriff, der leider ähnlich schwammig ist, insbesondere, seit er im Zusammenhang mit der komponentenbasierten Software-Entwicklung gesehen wird. Das IEEE-Glossar enthält eine recht allgemeine Definition dieses Begriffes.
component — One of the parts that make up a system. A component may be hardware or software and may be subdivided into other components.
IEEE Std 610.12 (1990)
Taylor, Medvidovic und Dashofy definieren den Begriff im Kontext von Software-Architekturen wie folgt:
A software component is an architectural entity that (1) encapsulates a subset of the system’s functionality and/or data, (2) restricts access to that subset via an explicitly defined interface, and (3) has explicitly defined dependencies on its required execution context.
Taylor, Medvidovic, Dashofy (2010)
Eine Komponente ist somit ein Bestandteil eines Systems und ein identifizierbares Element der Architektur.
Eine Komponente bietet ihrer Umgebung eine Menge von Diensten an, die über eine wohldefinierte Schnittstelle genutzt werden können (provided interface). Ihre internen Operationen und Daten macht sie unzugänglich (information hiding, siehe Abschnitt 17.3.3). Damit eine Komponente wiederverwendet werden kann, muss zudem angegeben werden, welche Dienste anderer Komponenten in Anspruch genommen werden (required interface).
Eine Komponente kann die Rolle eines Dienstanbieters (Server) oder die eines Dienstnutzers (Client) übernehmen, auch beide Rollen zugleich. Zwischen Server und Client besteht eine (gerichtete) Benutzt-Beziehung. Sie erlaubt es dem Client, alle Dienste und Daten zu benutzen, die der Server zur Verfügung stellt. Eine Komponente ist auch ein typisches »deliverable« für einen Programmierer oder für ein Programmierteam, also der Gegenstand eines Arbeitspakets.
Um Missverständnisse zu vermeiden, sprechen manche Autoren statt von einer Komponente von einem Software-Element, Software-Baustein oder auch Architekturbaustein.
Komponenten sind atomar oder werden verfeinert, d. h., eine Komponente kann weitere Komponenten enthalten. Beispiele für atomare Komponenten sind Klassen (Abschnitt 17.4.1) oder Module. Auch der Modulbegriff ist nicht klar. Eine sehr allgemeine Definition enthält die IEEE-Norm:
module — (1) A program unit that is discrete and identifiable with respect to compiling, combining with other units, and loading; for example, the input to, or output from an assembler, compiler, linkage editor, or executive routine.
(2) A logically separable part of a program.
IEEE Std 610.12 (1990)
Wir wollen diese Definition präziser fassen und definieren:
Ein Modul ist eine Menge von Operationen und Daten, die nur so weit von außen sichtbar sind, wie dies die Programmierer explizit zugelassen haben2.
Wo Komponenten zusammenarbeiten sollen, müssen sie zueinanderpassen. Hierfür ist die Metapher der Schnittstelle geläufig: Schneidet man einen Gegenstand, beispielsweise einen Apfel, in zwei Teile, so entstehen zwei Oberflächen, die spiegelsymmetrisch sind. Was durch einen Schnitt von selbst entsteht, die beiden exakt zueinanderpassenden Oberflächen, muss in der Technik hergestellt werden. Das englische Wort »interface« ist darum der Realität näher, wir konstruieren keine Schnittstellen, sondern Verbindungsstellen.
Eine sehr allgemeine Definition für »interface« geben Bachmann et al. (2002):
An interface is a boundary across which two independent entities meet and interact or communicate with each other.
Im Kontext von Komponenten wollen wir diese Definition konkretisieren und definieren:
Schnittstelle — Die Grenze zwischen zwei kommunizierenden Komponenten. Die Schnittstelle einer Komponente stellt die Leistungen der Komponente für ihre Umgebung zu Verfügung und/oder fordert Leistungen, die sie aus der Umgebung benötigt.
Das bedeutet auch: Eine Schnittstelle tut nichts und kann nichts. Ein Adapter (synonym: Konnektor), der dazu dient, nicht zueinanderpassende Komponenten zu verbinden, ist keine Schnittstelle, sondern hat selbst Schnittstellen.
Damit wir die Schnittstelle einer Komponente nutzen können, benötigen wir detaillierte Informationen, beispielsweise über die Struktur der Schnittstelle. Diese werden in der Schnittstellenbeschreibung dokumentiert. Sie enthält Angaben zu folgenden Aspekten:
Syntax und Art der Kommunikation: So müssen beispielsweise Name, Anordnung und Parametertypen der angebotenen Operationen angegeben werden.
Funktionale Merkmale: Dazu zählen die Dienste, die eine Komponente anbietet und die sie benötigt (z. B. die Rückgabe des Sinus-Wertes für den einzigen Parameter). Vor- und Nachbedingungen können angegeben werden, um die Dienste genauer zu beschreiben (Abschnitt 18.6.1). Zusätzlich muss das Verhalten im Fehlerfall spezifiziert werden.
Allgemeine Qualitätsangaben: Da die syntaktischen und funktionalen Merkmale nicht immer ausreichen, um zu klären, ob die Komponente in eine bestimmte Umgebung passt, muss eine Schnittstellenbeschreibung auch nichtfunktionale Anforderungen enthalten. Beispielsweise kann die Antwortzeit oder die Genauigkeit des Resultats kritisch sein. Das gilt natürlich in beiden Richtungen, wenn die Komponente Dienste der Umgebung beansprucht.
Um die syntaktischen Merkmale präzise zu beschreiben, verwenden wir formale Sprachen, sogenannte Interface Definition Languages. Bekannte Beispiele dafür sind die CORBA IDL und die Web Service Description Language (WSDL). Im einfachsten Fall können Schnittstellen direkt in der verwendeten Programmiersprache formuliert werden. So erlaubt es die Programmiersprache JAVA, Schnittstellen explizit (als interfaces) zu beschreiben. Die funktionalen und qualitativen Merkmale müssen natürlichsprachlich formuliert werden.
Das Wort »Entwurf« (oder »Architekturentwurf«) ist doppeldeutig, wie die nachfolgende Definition zeigt:
design — (1) The process of defining the architecture, components, interfaces, and other characteristics of a system or component.
(2) The result of the process in (1).
IEEE Std 610.12 (1990)
Wir sprechen immer dann vom Entwurf, wenn die Tätigkeit, deren Resultat die Architektur ist, im Vordergrund steht. Das entspricht der Bedeutung (1) der IEEE-Definition.
Software-Architektur wird von vielen Autoren definiert (vgl. die Webseiten des SEI, SEI-Architecture, o.J.). Im IEEE-Standard 1471 (2000) finden wir:
architecture — The fundamental organization of a system embodied in its components, their relationships to each other and to the environment, and the principles guiding its design and evolution.
IEEE Std 1471 (2000)
Bass, Clements und Kazman geben eine Definition des Software-Architekturbegriffs an, die in etwa konform zur oben zitierten ist und aus unserer Sicht die wesentlichen Aspekte enthält. Sie definieren:
The software architecture of a program or computing system is the structure or structures of the system which comprise software elements, the externally visible properties of those elements, and the relationships among them.
Bass, Clements, Kazman (2003)
Eine Software-Architektur besteht demnach aus Komponenten (auch »software elements« genannt) und ihren Beziehungen, insbesondere auch zur Umgebung, in verschiedenen Strukturen. So wie ein Gebäude eine tragende Struktur, ein Rohrleitungs- und ein Stromleitungsnetz enthält, so hat eine Software beispielsweise eine statische Struktur und eine Verteilungsstruktur der Komponenten. Die verschiedenen Strukturen werden durch Modelle beschrieben.
Da die Projektbeteiligten an sehr unterschiedlichen Informationen über die Architektur interessiert sind, führt der IEEE-Standard 1471 (2000) die Konzepte »Perspektive« (viewpoint) und »Sicht« (view) ein. Eine Sicht zeigt die Architektur eines Systems aus einer bestimmten Perspektive. Eine Perspektive fasst Anliegen und Informationsbedürfnisse zusammen, die durch die Interessen von Projektbeteiligten bestimmt sind. So ist beispielsweise der Projektleiter daran interessiert, wie die Entwicklung der Komponenten auf die Teams aufgeteilt werden kann. Die Betreiber des Systems hingegen möchten typischerweise wissen, auf welchen Rechnern das System installiert werden soll und welche Kommunikationsprotokolle vorgesehen sind. Entsprechende Sichten auf die Architektur liefern die geforderten spezifischen Informationen. Eine Sicht selbst fügt per definitionem keine neue Information zur Architektur hinzu, sondern fasst Informationen zusammen, die möglicherweise in verschiedenen Strukturen oder Modellen der Architektur enthalten sind.
In der Literatur gibt es verschiedene Vorschläge für Mengen von Perspektiven und Sichten, um Architekturen umfassend zu beschreiben; wegweisend ist das »4+1 View Model of Architecture« von Philippe Kruchten (1995). Vier Perspektiven haben sich in der Praxis bewährt:
Die Sichten der Systemperspektive stellen dar, wie das zu entwickelnde System in die Umgebung eingebettet ist und mit welchen anderen Systemen es wie interagiert. Dadurch sind die Systemgrenzen und die Schnittstellen zu den Nachbarsystemen festgelegt.
Die Sichten der statischen Perspektive zeigen die zentralen Komponenten der Architektur, ihre Schnittstellen und Beziehungen. In der Regel muss diese Zerlegung hierarchisch verfeinert werden, bis die einzelnen Komponenten so weit modularisiert sind, dass sie codiert werden können.
Die Sichten der dynamischen Perspektive stellen dar, wie die Komponenten zur Laufzeit zusammenarbeiten (Kontrollfluss, zeitliche Abhängigkeiten), sie zeigen also das Systemverhalten. Dabei beschränkt man sich auf die Modellierung der wichtigen Interaktionen. Die Frage, inwieweit dynamische Aspekte zur Software-Architektur gehören, wird unterschiedlich beantwortet. Eine Architektur hat einen Zweck, sie ist für eine bestimmte Funktion geschaffen. Diese Funktion diktiert die zentrale Anforderung, und die Architektur sollte gegen diese Anforderung geprüft werden. Trotzdem ist die Funktion nicht Teil der Architektur.
Die Sichten der Verteilungsperspektive zeigen, wie die Komponenten der statischen Sicht auf Infrastruktur- und Hardware-Einheiten abgebildet werden. Sie stellen aber auch dar, wie die Entwicklung, der Test etc. auf die Teams der Entwicklerorganisation aufgeteilt werden.
Die Wahl der Perspektiven und Sichten hängt natürlich von den Interessen und dem Informationsbedarf der Projektbeteiligten ab. Sind diese bekannt, dann kann festgelegt werden, welche Architekturinformationen in den unterschiedlichen Sichten enthalten sein sollen.
Zusammenfassend stellen wir fest: Die Architektur einer Software ist die Gesamtheit ihrer Strukturen auf einer bestimmten Abstraktionsebene; wo nichts anderes gesagt ist, geht es um die statische Struktur der höchsten Gliederungsebene, also um die Systemarchitektur.
Jede Architektur muss beschrieben werden, damit sie kommuniziert, bewertet und als Vorlage für die Codierung verwendet werden kann. Diese Beschreibung ist ein Modell der Architektur. Sie kann als Vorbild, d. h. als Bauplan für die Software, oder als Abbild der Architektur bei der Weiterentwicklung der Software genutzt werden. Der IEEE-Standard 1471 (2000) (siehe dazu Maier, Emery, Hilliard, 2001) stellt die Architekturbeschreibung in den Mittelpunkt der Betrachtung und definiert diese sehr allgemein wie folgt: »A collection of products to document an architecture.« Ellis et al. geben eine Definition für Architekturbeschreibung und Sichten an, die uns sinnvoll scheint:
An architectural description is a model – document, product or other artifact – to communicate and record a system’s architecture. An architectural description conveys a set of views each of which depicts the system by describing domain concerns.
Ellis et al. (1996)
Um Architekturen zu beschreiben, können spezielle Architekturbeschreibungssprachen (Architecture Description Language, ADL) verwendet werden. Diese bieten eine Notation in Form einer (meist grafischen) Syntax. Die dazugehörende Semantik ist jedoch leider nicht immer präzise definiert. Mit einer bestimmten ADL kann man typischerweise eine Sicht oder mehrere Sichten einer Architektur modellieren. Medvidovic und Rosenblum definieren diesen Begriff wie folgt:
An ADL for software applications focuses on the high-end structure of the overall application rather than the implementation details of any specific source module. ADLs provide both a concrete syntax and a conceptual framework for modelling a software system’s conceptual architecture.
Medvidovic, Rosenblum (1997)
Beginnend mit der Publikation von DeRemer und Kron (1976) hat das Software Engineering eine Reihe von Architekturbeschreibungssprachen hervorgebracht, die allerdings kaum eingesetzt wurden. Erst in den letzten Jahren hat sich die Sprache UML für die Beschreibung objektorientierter Architekturen durchgesetzt. An UML gibt es viel – begründete – Kritik (Parnas: »Undefined Modeling Language«), und nicht alle Aspekte der Architektur lassen sich in UML darstellen. Mangels einer Alternative sollte man UML trotzdem nutzen, wo es möglich ist.
In den vorangegangenen Abschnitten haben wir zentrale Begriffe des Software-Entwurfs eingeführt; sie bilden einen Teil der Fachsprache eines Software-Architekten. Damit man leichter erkennt, wie die Begriffe zusammenhängen, haben wir sie in einem Begriffsmodell angeordnet.
Abb. 17–2 Begriffe des Software-Entwurfs
Wenn wir über die Eigenschaften einer Architektur sprechen, so stellen wir uns das System vor, das mit dieser Architektur entstehen soll. Die Software-Architektur ist also gut, wenn die an die Software gestellten funktionalen und nichtfunktionalen Anforderungen erfüllt werden können. Die Einhaltung der Anforderungen sollte zudem möglichst einfach zu prüfen sein. Beispielsweise erlaubt eine Architektur, die alle Abhängigkeiten vom Betriebssystem in einer Komponente konzentriert, eine leichte Überprüfung der Portabilitätsanforderungen.
Es gibt keine Entwurfsmethode, die uns diese Fragen allgemeingültig beantwortet und eine gute Software-Architektur garantiert, aber wir kennen Entwurfsprinzipien, die sich bewährt haben und die ein Software-Architekt beachten und anwenden sollte. Sie helfen ihm, die folgenden Fragen zu klären:
Nach welchen Kriterien soll das System in Komponenten unterteilt werden?
Welche Aspekte sollen in Komponenten zusammengefasst werden?
Welche Dienstleistungen sollen Komponenten nach außen an ihrer Schnittstelle anbieten, welche Aspekte müssen geschützt sein?
Wie sollen die Komponenten miteinander interagieren?
Wie sollen Komponenten strukturiert und verfeinert werden?
Nachfolgend werden wichtige Entwurfsprinzipien erläutert.
Wie in Abschnitt 17.2.4 definiert wurde, gliedert die Architektur eine Anwendung in sinnvolle und überschaubare Bestandteile, die Komponenten. Eine Komponente ist in der Regel intern strukturiert, sie besteht entweder aus weiteren Komponenten oder aus Modulen. Das Aufteilen des Systems in seine Komponenten wird als Modularisierung bezeichnet.
modular decomposition — The process of breaking a system into components to facilitate design and development; an element of modular programming.
IEEE Std 610.12 (1990)
Eng verbunden damit ist der Begriff der Modularität.
modularity — The degree to which a system or computer program is composed of discrete components such that a change to one component has minimal impact on other components.
IEEE Std 610.12 (1990)
Modularität ist demnach eine Eigenschaft der Architektur. Sie ist hoch, wenn es dem Architekten gelungen ist, das System so in Komponenten zu zerteilen, dass diese möglichst unabhängig voneinander verändert und weiterentwickelt werden können.
Parnas hat wegweisende Arbeiten zur Modularisierung geleistet (Parnas, 1972). Folgende Ziele werden danach mit dem modularen Entwurf angestrebt:
Die Struktur jedes Moduls soll einfach und leicht verständlich sein.
Die Implementierung eines Moduls soll austauschbar sein; Information über die Implementierung der anderen Module ist dazu nicht erforderlich. Die anderen Module sind vom Austausch nicht betroffen.
Die Module sollen so entworfen sein, dass wahrscheinliche Änderungen ohne Modifikation der Modulschnittstellen durchgeführt werden können.
Große Änderungen sollen sich durch eine Reihe kleiner Änderungen realisieren lassen. Solange die Schnittstellen der Module nicht verändert sind, soll es möglich sein, alte und neue Modulversionen miteinander zu testen.
Abb. 17–3 Modularten am Beispiel
Nach den Entwurfsentscheidungen, durch die sie entstanden sind, kann man verschiedene Modularten unterscheiden (Abb. 17–3). In Nagl (1990) wird die folgende Klassifikation vorgeschlagen:
Funktionale Module gruppieren Berechnungen, die logisch zusammengehören. Sie haben kein »Gedächtnis«, also keinen variablen Zustand, d. h., die Wirkungsweise der an der Schnittstelle angebotenen Berechnungsfunktionen ist nicht abhängig vom vorherigen Programmablauf. Beispiele für solche Module sind Sammlungen mathematischer Funktionen oder Transformationsfunktionen.
Datenobjektmodule realisieren das Konzept der Datenkapselung. Ein Datenobjektmodul versteckt dazu Art und Aufbau der Daten und stellt an seiner Schnittstelle Operationen zur Verfügung, um die gekapselten Daten zu manipulieren. Beispiele sind Module, die globale Konfigurationsdaten kapseln, und Module, die gemeinsam benutzte Datenverwaltungen realisieren (z. B. ein Produktkatalog). Auch jede Datenbank ist ein Datenobjektmodul.
Datentypmodule implementieren, wie der Name sagt, einen benutzerdefinierten Datentyp in Form eines Abstrakten Datentyps. Dadurch ist es möglich, beliebig viele Exemplare des Datentyps zu erzeugen und zu benutzen. Beispiele sind Module für die Datentypen Kunde oder Produkt.
Bei einem objektorientierten Entwurf entspricht die Klasse dem Datentypmodul. Ein Datenobjektmodul entspricht einer Klasse, die lediglich Klassenmethoden anbietet und von der keine Objekte erzeugt werden können. Mit dem Entwurfsmuster Singleton (siehe Abschnitt 17.5.2) können ebenfalls Datenobjektmodule realisiert werden. Funktionale Module sind beim objektorientierten Entwurf eher die Ausnahme (z. B. die Klasse Math der JAVA-Klassenbibliothek), da Klassen so angelegt sind, dass sie Daten und dazugehörende Operationen kapseln.
Der Architekt eines Konzertsaals bemüht sich, den Saal so zu bauen, dass die akustische Kopplung nach außen sehr gering ist, damit kein Lärm hinein- oder – etwa bei Rockkonzerten – hinausdringt. Innerhalb des Saals soll dagegen der akustische Zusammenhalt hoch sein, damit auch auf den billigen Plätzen noch das leiseste Pianissimo hörbar ist.
Analog versucht der Software-Architekt, die Module so zu entwerfen, dass die (inter-modulare) Kopplung (d. h. die Breite und Komplexität der Schnittstellen zwischen den Modulen) möglichst gering, der (intra-modulare) Zusammenhalt (d. h. die Verwandtschaft zwischen den Bestandteilen eines Moduls) möglichst hoch wird.
Man kann die Module auch mit Knödeln vergleichen, die beim Kochen dazu neigen, zusammenzukleben oder zu zerfallen. Das ideale Modul klebt nicht (geringe Kopplung) und zerfällt nicht (hoher Zusammenhalt).
Tab. 17–1 Stufen der Kopplung (von stark = schlecht nach schwach = gut)
Stufe |
Kopplung zwischen Prozeduren / Modulen |
erreichbar für |
Einbruch |
Der Code kann verändert werden |
??? |
volle Öffnung |
Auf alle Daten, z. B. auf globale Daten in einem C-Programm, kann zugegriffen werden |
(Module) Prozeduren |
selektive Öffnung |
Bestimmte Variablen sind zugänglich, global oder durch expliziten Export und Import |
Module, Prozeduren |
Prozedurkopplung |
Die Prozeduren verschiedener Module sind nur durch Parameter oder Funktionen gekoppelt |
Module (Prozeduren) |
Funktionskopplung |
Die Prozeduren verschiedener Module sind nur durch Wertparameter und Funktionsresultate gekoppelt |
Module (Prozeduren) |
keine Kopplung |
Es besteht keine Beziehung zwischen den Modulen, der Zugriff ist syntaktisch unmöglich |
Module |
Dieses Prinzip wurde mit Structured Design (Stevens, Myers, Constantine, 1974) publiziert, es geht daher von einer FORTRAN- oder COBOL-Implementierung mit zweistufiger Hierarchie aus (System und Subroutines). Die Tabellen 17–1 (Kopplung) und 17–2 (Zusammenhalt) sind angelehnt an Macro und Buxton (1987), S. 170. Die Tabellen beziehen sich teilweise auf Probleme, die in modernen Programmiersprachen gar nicht auftreten können. Beispielsweise ist der Einbruch, die Code-Veränderung, nur auf primitivster Assembler-Ebene möglich.
Tab. 17–2 Stufen des Zusammenhalts (von schwach = schlecht nach stark = gut)
Stufe |
Zus.halt innerhalb einer Prozedur / eines Moduls |
erreichbar für |
kein Zusammenhalt |
Die Zusammenstellung ist zufällig |
Module |
Ähnlichkeit |
Die Teile dienen z. B. einem ähnlichen Zweck (alle Operationen auf Matrizen oder zur Fehlerbehandlung) |
Module |
zeitliche Nähe |
Die Teile werden zur selben Zeit ausgeführt (Initialisierung, Abschluss) |
Module, Prozeduren |
gemeinsame Daten |
Die Teile sind durch gemeinsame Daten verbunden, auf die sie nicht exklusiven Zugriff haben |
Module, Prozeduren |
Hersteller/ Verbraucher |
Der eine Teil erzeugt, was der andere verwendet |
Module, Prozeduren |
einziges Datum (Kapselung) |
Die Teile realisieren alle Operationen, die auf einer gekapselten Datenstruktur möglich sind |
Module, Prozeduren |
einzige Operation |
Operation, die nicht mehr zerlegbar ist |
Prozeduren |
Eine hohe Kopplung bewirkt, dass Korrekturen und Änderungen über mehrere Einheiten »verschmiert« sind, die Wartung ist entsprechend aufwändig und unsicher. Eine Einheit mit geringem Zusammenhalt könnte ohne Nachteil weiter aufgespalten werden und wäre dann leichter und sicherer zu verstehen, zu korrigieren und zu warten. Geringe Kopplung und hoher Zusammenhalt bewirken also gleichermaßen hohe Lokalität und damit gute Wartbarkeit.
Wenn wir modulare Programme betrachten, müssen wir die Aussagen über Programmkomponenten auf die Prozeduren oder auf die Module übertragen. Es liegt nahe, von den Modulen geringe Kopplung, von den Prozeduren hohen Zusammenhalt zu fordern. Die linke Seite der Abbildung 17–4 zeigt schematisch ein solches Programmsystem. Dass die Kopplung zwischen Prozeduren im selben Modul nicht gering sein kann, ist klar, denn alle Prozeduren greifen auf die lokalen Variablen des Moduls zu. Natürlich ist auch der Zusammenhalt zwischen den Prozeduren eines Moduls geringer als der Zusammenhalt innerhalb einer Prozedur.
Abb. 17–4 Kopplung und Zusammenhalt, links im modularen, rechts im objektorientierten Programm
Wenn wir diese Aussagen auf objektorientierte Programme übertragen wollen, können wir nicht einfach »Modul« durch »Klasse« und »Prozedur« durch »Methode« ersetzen (rechte Seite der Abb. 17–4). Denn während Prozeduren noch relativ unabhängig von anderen Prozeduren betrachtet (implementiert, analysiert, geprüft) werden können, sind die Methoden einer Klasse sehr eng miteinander verknüpft. Darum betrachten wir in objektorientierten Programmen sowohl bei der Kopplung als auch beim Zusammenhalt die Klassen. Sie sollten untereinander nur lose gekoppelt sein und jeweils aus Methoden bestehen, die starken Zusammenhalt haben.
Kopplung und Zusammenhalt kann man messen, wenn die Definitionen syntaktisch konkretisiert, also in Metriken umgesetzt werden; das ist für objektorientierte Programme versucht worden (siehe Abschnitt 14.4.5).
Vor allem im militärischen Bereich gilt seit langem das Prinzip, nur so viel Wissen weiterzugeben, wie der Adressat benötigt, um seine Funktion auszuüben (»Need-to-know-Prinzip«). Auf diese Weise wird die Gefahr vermindert, dass Informationen denen zugetragen werden, die sie (aus Sicht des Urhebers) missbrauchen.
Parnas (1972) hat dieses Prinzip im Software Engineering eingeführt. Auch hier geht es darum, den Missbrauch von Information zu verhindern. Dabei wird dem Empfänger keineswegs feindliches Verhalten unterstellt; schon der – weitverbreitete und kaum auszuschließende – leichtfertige Umgang mit Informationen schafft ein großes Problem.
Ein Programmierer, dessen Programm sehr oft bestimmte Daten benötigt, wird versuchen, den Zugriff sehr effizient zu implementieren. Er könnte beispielsweise ausnutzen, dass die Informationen auf bestimmten Positionen in einem Feld gespeichert sind. Sein Programm greift also direkt auf die Daten zu. Das hat (geringe) Vorteile, bis eines Tages die Speicherung der Daten verändert wird (weil der Umfang der Daten stark gewachsen ist, weil weitere Details gespeichert werden sollen o. Ä.). In diesem Moment hat die Lösung große Nachteile, denn plötzlich versagt ein Programm, das zuvor scheinbar in Ordnung war. Ein Programmierer darf also niemals Informationen über die Details der Datenorganisation ausnutzen. Andernfalls kann das Programm nicht mehr verändert werden. Aus diesem Grund ist es praktisch kaum möglich, in einem großen, traditionell strukturierten Programm die Datenstrukturen einer veränderten Situation anzupassen; die Zahl der notwendigen Änderungen ist unüberschaubar, und die betroffenen Stellen des Programms sind kaum zu lokalisieren.
Die Idee des »Information Hiding« besteht darin, dem Programmierer Informationen, die er nicht verwenden darf, gar nicht erst zugänglich zu machen.
information hiding — A software development technique in which each module’s interfaces reveal as little as possible about the module’s inner workings, and other modules are prevented from using information about the module that is not in the module’s interface specification.
IEEE Std 610.12 (1990)
Abb. 17–5 Situation ohne Kapselung von Daten
Ein Modul stellt durch Operationen an seiner Schnittstelle nur genau das zur Verfügung, was seine Kundenmodule (die Module, die seine Leistungen in Anspruch nehmen) wirklich benötigen. Ein Kundenmodul greift also nicht direkt auf eine Variable zu, sondern ruft eine Operation auf, die den gewünschten Effekt hat, z. B. einen Wert liefert (Abb. 17–6).
Abb. 17–6 Modul mit gekapselten Daten
Wird die Darstellung der Daten verändert, so müssen nur diese Operationen angepasst werden; die Kundenmodule sind – außer vielleicht durch eine veränderte Antwortzeit – nicht betroffen, solange die Schnittstelle des Moduls unverändert bleibt. Verheimlicht wird also die Darstellung der Information, nicht die (abstrakte) Information selbst, die ein Kundenmodul braucht. Die Zugriffe von außen erfolgen ausschließlich indirekt über die Operationen, die als Vermittler dienen. Die Bezeichnung »Information Hiding« ist darum unglücklich, denn sie suggeriert, dass wichtige Information zurückgehalten wird. Tatsächlich wird nur eigentlich belanglose Information geschützt. Wenn wir eine Telefonnummer wählen, wissen wir über die Leitungen für unser Gespräch nichts. Darum kann es uns auch gleichgültig sein, ob wir beim nächsten Gespräch über andere Leitungen verbunden werden.
Der Schutz, der durch das Information Hiding erzielt wird, ist natürlich nur partiell, verhindert werden vor allem unbeabsichtigte Fehler. Sabotage ist weiterhin möglich, denn auch die vorgesehenen Operationen können sinnvoll oder falsch verwendet werden. Aber auch bei Sabotage erleichtert das Information Hiding die Diagnose; da alle Zugriffe über Operationen geschehen, können dort Protokoll- und Kontrollmechanismen eingebaut werden. Wenn Fehler in den Zugriffsoperationen liegen, ist die Fehlersuche wesentlich erleichtert, weil der zu untersuchende Code klar begrenzt ist.
Die folgende Analogie soll die Wirkungsweise und den Nutzen des Information Hiding verdeutlichen: Ein Bankkonto liegt voll in der Kontrolle der Bank, nur diese kann tatsächlich Veränderungen daran vornehmen. Der Kunde hat keinen direkten Zugang zum Kassenraum, er beauftragt die Bankangestellten, die Transaktionen durchzuführen, die er in Anspruch nehmen will. Auf diese Weise ist sichergestellt, dass der Kunde keine unzulässigen Transaktionen ausführt. Ändert sich die interne Organisation der Bank, so müssen nur die Angestellten davon unterrichtet werden, der Kunde ist nicht betroffen.
Auch bei Banken kann es vorkommen, dass ein Angestellter aus der Kasse Geld stiehlt. Das Verfahren schafft also keine totale Sicherheit. Wenn sich aber zeigt, dass Geld fehlt, muss man nur die Angestellten überprüfen, nicht die Kunden, denn diese hatten keine Möglichkeit, an das Geld zu kommen. Damit ist die Suche nach dem Täter relativ einfach. Das Prinzip der Geldverwaltung in der Bank verhindert allerdings nicht, dass der Kunde sein Geld unsinnig verwendet oder sich mit Krediten übernimmt. Wenn aber nach den Personen hinter verdächtigen Transaktionen gefahndet wird, ist die auf der wohlorganisierten Abwicklung basierende Buchführung der Banken äußerst nützlich.
Man kann sich über die Vor- und Nachteile von Sicherheitsgurten im Auto unterhalten; jeder vernünftige Mensch wird zustimmen, dass die Vorteile bei weitem überwiegen. Ähnlich ist es beim Information Hiding. Die großen Vorteile sind:
Weil die Datenstrukturen und die dazu gehörenden Zugriffsoperationen zusammen in einem einzigen Modul abgelegt werden, ist die Lokalität des Programms wesentlich verbessert. Damit sinkt die Gefahr unbeabsichtigter Fehlmanipulationen deutlich. Die Datenstrukturen können erheblich einfacher geändert werden als bei unkontrolliertem Zugriff.
Die Zugriffe können überwacht werden, ohne dass man in die tieferen Abstraktionsebenen eindringen, also einen Debugger verwenden muss. Ein Debugger ist ein mächtiges, aber gefährliches Werkzeug, weil es den Benutzer veranlasst, das Programm auf sehr tiefem Abstraktionsniveau zu verstehen und zu bearbeiten. Wir setzen ihn als letztes Mittel ein, als Ultima Ratio, wenn wir keine andere Wahl haben, beispielsweise, wenn wir einen Compiler-Fehler vermuten.
Die (in der Regel akzeptablen) Nachteile des Information Hiding sind:
Der Aufruf der vermittelnden Operationen kostet unvermeidlich Rechenzeit, das Programm ist dadurch weniger effizient. Parnas selbst hat 1972 darauf hingewiesen; die technische Entwicklung hat diesen Einwand entkräftet. Grundsätzlich wäre es auch möglich, die Effizienzeinbußen durch optimierende Übersetzer ganz zu vermeiden, denn wenn eine Funktion nur einen ohnehin verfügbaren Wert liefert, kann sie auch durch einen direkten Zugriff ersetzt werden. Das wäre für den Programmierer unsichtbar und damit unschädlich.
Bei konsequentem Information Hiding entstehen sehr viele Module (leicht einige Hundert), und der Überblick, der gerade durch das Information Hiding geschaffen oder verbessert werden sollte, leidet. Parnas und seine Mitarbeiter haben nach einigen Erfahrungen mit dem Information Hiding vorgeschlagen, die Module in einem Modul-Handbuch (module guide) zu charakterisieren (Parnas, Clements, Weiss, 1985).
Werden Module nach dem Prinzip des Information Hiding entworfen, dann ist es natürlich geboten, dieses Prinzip auch bei der Codierung der Module beizubehalten. In Abschnitt 18.4 zeigen wir, wie Information Hiding im Programmcode realisiert wird.
Die Trennung von Zuständigkeiten (separation of concerns) ist ein grundlegendes Prinzip im Software Engineering. Für den Software-Entwurf ist es von zentraler Bedeutung, denn jede Komponente sollte nur für einen ganz bestimmten Aufgabenbereich zuständig sein. Komponenten, die gleichzeitig mehrere Aufgaben abdecken, sind oft unnötig komplex. Das erschwert das Verständnis und damit die Wartung und Weiterentwicklung und verhindert, dass diese Komponenten wiederverwendet werden können.
Es gibt unterschiedliche Kriterien, um Zuständigkeiten zu trennen. Beim objektorientierten Entwurf sind die Daten und die Operationen darauf das Hauptkriterium: Zusammengehörende Daten und Operationen werden in einer Klasse zusammengefasst und von anderen Klassen getrennt. Oft werden auch spezielle funktionale Aspekte (Features), beispielsweise das Drucken, das Speichern oder die Visualisierung, als eigenständige Komponenten modelliert (Abb. 17–7).
Weitere wichtige Regeln zur Trennung von Zuständigkeiten sind:
Trenne fachliche und technische Komponenten. Diese Regel ist die konsequente Weiterführung der Überlegungen zur idealen Technologie (Abschnitt 16.7.2).
Ordne Funktionalitäten, die variabel sind oder später erweitert werden müssen, eigenen Komponenten zu.
Abb. 17–7 Klassen modellieren getrennte Zuständigkeiten
Auch die Trennung von Funktion und Interaktion ist eine Ausprägung dieses Prinzips. Alle interaktiven Programme enthalten neben den Komponenten, die die Funktionalität realisieren, auch Komponenten für die Interaktion. Die Trennung der Zuständigkeiten bedeutet hier, dass Interaktion und Funktionalität strikt gegeneinander abgeschottet werden. Dadurch wird es möglich, die Funktion oder die Interaktion zu verändern, ohne den anderen Teil des Programms zu gefährden.
Besonders wenn Anwendungen von der Bedienoberfläche her entworfen und implementiert werden (also outside-in, Abschnitt 17.1.4), wird die Funktionalität oft unterschätzt und darum nicht sauber separiert. Das führt unweigerlich zu Problemen (Bäumer et al., 1996); Programme, in denen Funktionalität und Interaktion durchmischt sind, machen die Wartung unnötig schwierig. Die Trennung von Interaktion und Funktion ist darum ein zentrales Entwurfsprinzip für die Konstruktion interaktiver Anwendungen. Im Model-View-Controller-Architekturmuster ist sie exemplarisch umgesetzt (Abschnitt 17.5.1).
Die Komponenten einer Architektur müssen so lange verfeinert werden, bis sie so einfach sind, dass wir sie spezifizieren und realisieren können. Die hierarchische Gliederung ist ein bewährtes Vorgehen, um Komplexität zu reduzieren.
Eine Hierarchie ist eine Struktur von Elementen, die durch eine (hierarchiebildende) Beziehung in eine Rangfolge gebracht werden. Entsteht dadurch eine Baumstruktur, dann spricht man von einer Monohierarchie, d. h., jedes Element der Hierarchie außer dem Wurzelelement besitzt genau ein übergeordnetes Element. Kann ein Element mehrere übergeordnete Elemente haben, dann handelt es sich um eine Polyhierarchie; die entsprechende Struktur ist ein azyklischer gerichteter Graph (Wikipedia, o.J.). Im Kontext des Software-Entwurfs verwenden wir drei Arten von Hierarchien:
Eine Aggregationshierarchie gliedert ein System oder eine Komponente in ihre Bestandteile; sie wird auch »Ganzes-Teile-Hierarchie« genannt. Die Beziehung zwischen dem Ganzen und seinen Teilen ist von der Art besteht-aus (in der Gegenrichtung ist-Teil-von). Wir entwerfen Aggregationshierarchien immer auf der höchsten Ebene, der Ebene der Systemarchitektur. Sie teilen das System in Komponenten auf, die hierarchisch weiter verfeinert werden. Aggregationshierarchien sind in der Regel Monohierarchien.
Eine Schichtenhierarchie ordnet Komponenten (die dann als Schichten bezeichnet werden) derart, dass jede Schicht genau auf einer darunterliegenden Schicht aufbaut und die Basis für genau eine darüberliegende Schicht bildet (siehe Abschnitt 17.5.1). Eine Schichtenhierarchie ist damit eine strengere Form einer Monohierarchie.
Eine Generalisierungshierarchie ordnet Komponenten nach Merkmalen (Funktionen und Attributen), indem fundamentale, gemeinsame Merkmale mehrerer Komponenten in einer universellen Komponente zusammengefasst werden. Davon abgeleitete, spezialisierte Komponenten übernehmen diese Merkmale und fügen spezielle hinzu. Damit werden die fundamentalen Merkmale nur einmal definiert. Generalisierungshierarchien können als Monooder als Polyhierarchien auftreten. Der objektorientierte Entwurf stellt Generalisierungshierarchien von Klassen und Schnittstellen in den Vordergrund, da diese mit Hilfe der Vererbung direkt in einer objektorientierten Programmiersprache implementiert werden können (siehe Abschnitt 17.4.5).
Abb. 17–8 Beispiele für Hierarchien
Abbildung 17–8 zeigt links eine Architektur, die alle oben beschriebenen Hierarchiearten enthält. Die Aggregationshierarchie ist implizit durch die Topologie der Elemente angegeben (System A besteht aus den Schichten S1 und S2, die Schicht S1 aus den Komponenten K1, K2 und K3). Während die Schichtenhierarchie ebenfalls durch die Topologie angegeben wird, sind die Generalisierungsbeziehungen zwischen den Komponenten K1 und K2 sowie K1 und K3 explizit dargestellt. Auf der rechten Seite sind die Elemente der Architektur, alle Beziehungen und die dadurch entstandenen Hierarchien abgebildet. Wie man sieht, kann ein Element in mehreren Hierarchien enthalten sein. So ist beispielsweise die Komponente K2 Element der Aggregations- und der Generalisierungshierarchie.
Wir brauchen alle Arten von Hierarchien. Auch wenn wir objektorientiert entwerfen, können wir nicht auf Aggregations- und Schichtenhierarchien verzichten.
Die hier vorgestellten Entwurfsprinzipien stehen miteinander in Beziehung, und ein Software-Architekt wendet typischerweise gleichzeitig mehrere dieser Prinzipien an. Die konzeptuelle Grundlage dieser – und vieler anderer – Entwurfsprinzipien ist die Abstraktion. Indem wir Abstraktionen bilden, konzentrieren wir uns auf das Wesentliche und blenden das Unwesentliche aus. Es ist eine wichtige Fähigkeit jedes guten Architekten, in Abstraktionen zu denken und Abstraktionen zu bilden.
Abb. 17–9 Entwurfsprinzipien und ihr Zusammenhang
Abbildung 17–9 zeigt die beschriebenen Entwurfsprinzipien und ihre Zusammenhänge. Die Modularisierung steht im Zentrum, die anderen Prinzipien tragen dazu bei, den Prozess der Modularisierung effektiv zu unterstützen.
Das Ziel des objektorientierten Entwurfs besteht darin, fachliche Programmbausteine (Klassen) so zu entwerfen, dass sie die relevanten Gegenstände und Konzepte des betrachteten Anwendungsbereichs repräsentieren. Technisch gesehen, werden die Klassen konsequent nach dem Prinzip des Information Hiding (Abschnitt 17.3.3) konstruiert, d. h., sie verstecken getroffene Entwurfsentscheidungen und Interna und bieten an ihrer öffentlichen Schnittstelle einen Satz fachlich motivierter Dienstleistungen an.
Ein wesentlicher Vorteil der objektorientierten Software-Entwicklung liegt darin, dass die objektorientierten Modellierungskonzepte durchgehend von der Analyse über den Entwurf bis hin zur Codierung genutzt werden können. Sie lassen sich unabhängig vom Entwurfsvorgehen und von der benutzten Programmiersprache in einem Metamodell beschreiben.
Ein objektorientiertes Metamodell enthält nach Züllighoven (2005) die Elemente und Beziehungen, die die objektorientierte Modellierung zur Verfügung stellt, sowie Regeln, die beschreiben, wie sie bei der Modellierung zu verwenden sind. Abbildung 17–10 zeigt den Kern eines objektorientierten Metamodells. Die zentralen Elemente und Beziehungen werden nachfolgend beschrieben.
Abb. 17–10 Metamodell der Objektorientierung
Objekte und Nachrichten
Ein Objekt repräsentiert aus fachlicher Sicht einen Gegenstand der Anwendung mit allen seinen Eigenschaften. Technisch werden die Dienste, die das Objekt anbietet, durch Methoden beschrieben; die Daten, die das Objekt und seinen Zustand charakterisieren, werden in Attributen gespeichert. Objekte kommunizieren miteinander, indem sie Nachrichten versenden und empfangen. Hat ein Objekt eine Nachricht empfangen, wird die Methode ausgeführt, die die Klasse dieses Objekts für diese Nachricht bereithält.
Jedes Objekt ist ein Exemplar (eine Instanz) genau einer Klasse. Sie definiert die Attribute der Objekte und ihre Methoden. Klassen können sich selbst aber auch wie Objekte verhalten, d. h., sie können Nachrichten empfangen und versenden. Dementsprechend besitzen auch Klassen eigene Attribute und Methoden. Wir unterscheiden also zwischen Objekt- und Klassenattributen sowie zwischen Objekt- und Klassenmethoden.
Generalisierung
Die Generalisierungsbeziehung ist aus der Sicht der Modellierung ein wesentliches Merkmal, das den objektorientierten vom konventionellen Entwurf unterscheidet. Die Generalisierungsbeziehung besteht zwischen Klassen, die damit hierarchisch organisiert werden können. Ist die Klasse A eine Generalisierung der Klasse B, dann wird A als Ober- und B als Unterklasse bezeichnet. Die Unterklasse hat grundsätzlich alle Eigenschaften, die die Oberklasse besitzt. Sie kann weitere (spezielle) Eigenschaften hinzufügen und auch Eigenschaften der Oberklasse überschreiben (redefinieren).
Abstrakte Klassen
Wird die Generalisierungsbeziehung konsequent eingesetzt, dann entstehen Klassen, die nur abstrakte Konzepte oder Begriffe repräsentieren. Diese werden als abstrakte Klassen bezeichnet. Charakteristisch für abstrakte Klassen sind Methoden, die nicht definiert (realisiert), sondern nur durch die Angabe der Signaturen spezifiziert sind. Diese werden abstrakte Methoden genannt; ihre Realisierung ist jeder einzelnen Unterklasse auferlegt, wenn diese nicht ebenfalls abstrakt ist. Abstrakte Klassen haben keine Exemplare.
Benutzt- und Aggregationsbeziehungen
Damit Objekte die angebotenen Dienstleistungen anderer Objekte nutzen können, müssen entsprechende Beziehungen auf Klassenebene geknüpft werden, die Benutzt-Beziehungen. Die Aggregationsbeziehung modelliert eine Teile-Ganzes-Beziehung zwischen Objekten. Mit der Aggregationsbeziehung können Objekte hierarchisch strukturiert werden. Man unterscheidet bei Teile-Ganzes-Beziehungen, ob ein Teil in mehreren Ganzen enthalten sein kann oder ob ein Teil ausschließlich einem Ganzen zugeordnet ist. Den letzten Fall, die restriktive Form der Aggregation, bezeichnet man auch als Komposition.
Abbildung 17–11 zeigt eine Klassenstruktur (nur einen Ausschnitt), die Audiogeräte objektorientiert modelliert. Wir haben die Gemeinsamkeiten aller konkreten Audiogeräte in der Klasse Audiogeraet zusammengefasst; sie ist die (abstrakte) Oberklasse aller Arten von Audiogeräten. Die Klasse definiert, dass alle Audiogeräte eine Seriennummer haben; diese wird in einem Objektattribut abgelegt. Ferner gibt die Klasse Audiogeraet beispielsweise durch die abstrakte Methode start allen Unterklassen vor, dass es mit dieser Nachricht möglich sein muss, die Tonwiedergabe zu starten, obwohl die konkreten Schritte dazu vom jeweiligen Gerätetyp abhängen. Weiterhin legt sie fest, dass jedes Audiogerät ein Netzteil enthält; dies wird mit einer Kompositionsbeziehung modelliert.
Abb. 17–11 Beispiel einer Klassenstruktur
Die Generalisierungsbeziehung wird benutzt, um spezielle Audiogeräte in Form einer Hierarchie zu modellieren. Die Klassen CDSpieler und Schallplattenspieler sind Spezialisierungen der abstrakten Klasse Audiogeraet; die Klasse CDRekorder ist wiederum eine Spezialisierung der Klasse CDSpieler. Betrachten wir exemplarisch die Klasse CDSpieler. Sie definiert die von ihrer Oberklasse vorgegebenen abstrakten Methoden (start und stopp), erweitert diese um spezielle Methoden (z. B. pause) und fügt neue Objektattribute hinzu, um die Titelanzahl und den aktuellen Titel zu speichern. Zusätzlich wird modelliert, dass alle CD-Spieler ein Laufwerk enthalten und die gleiche Abtastrate haben. Diese kann darum in einem Klassenattribut gespeichert werden.
Das Ergebnis des objektorientierten Entwurfs ist immer eine Architektur, die aus Klassen und ihren Beziehungen besteht. Publikationen wie beispielsweise Booch et al. (2007) unterteilen den objektorientierten Entwurf in vier zentrale Aktivitäten:
a) Identifizieren von Objekten und Klassen
b) Festlegen des Verhaltens der Objekte und Klassen
c) Identifizieren von Beziehungen zwischen den Klassen
d) Festlegen der Schnittstellen zwischen den Klassen
Natürlich genügt es nicht, diese Tätigkeiten einmal sequenziell zu durchlaufen; sie werden so oft wiederholt, bis die Klassenstruktur ausreichend detailliert ist, damit sie als Basis für die Codierung herangezogen werden kann.
Die richtige Wahl der Klassen ist der wichtigste Schritt zu einem guten objektorientierten Entwurf. Wir wollen diesen Schritt nachfolgend am Beispiel einer Anwendungssoftware zeigen. Anwendungssoftware enthält immer ein Modell der Realität, d. h. ein Modell ihres Anwendungsbereichs. Die Software ist damit eng an die Anwendung gekoppelt; wenn sich die Anwendung ändert, muss diese Änderung in der Software ebenfalls vollzogen werden. Darum ist es zweckmäßig, die Konzepte und Begriffe des Anwendungsbereichs (der typischerweise eine eigene Fachsprache hat) in der Architektur explizit zu modellieren. Beispiele für Anwendungsbereiche sind die Lohnbuchhaltung, der Einkauf oder die Kundenbetreuung.
Im Anwendungsbereich wird die Ist-Analyse durchgeführt und das fachliche Modell entwickelt. Es besteht im Kern aus den fachlichen Abläufen (den Funktionen oder Geschäftsprozessen) und den relevanten Gegenständen. Die fachlichen Abläufe kann man in Use Cases modellieren, die Gegenstände des Anwendungsbereichs und ihre Beziehungen werden durch ein objektorientiertes Begriffsmodell beschrieben.
Betrachten wir dazu als Beispiel eine Software zur Verwaltung von Lehrveranstaltungen und Prüfungen an einer Hochschule. Ein Ausschnitt der fachlichen Funktionen ist in Abbildung 17–12 in Form eines Use-Case-Diagramms dargestellt. Ein Dozent kann eine Lehrveranstaltung und eine Prüfung anmelden, er kann Räume für Lehrveranstaltungen oder Prüfungen reservieren. Ein Student kann sich für eine Lehrveranstaltung anmelden, er kann sich für eine Prüfung anoder sich von ihr abmelden und alle seine Prüfungsleistungen auflisten. Abbildung 17–13 zeigt einen Ausschnitt der relevanten Begriffe und ihrer Beziehungen in Form eines fachlichen Begriffsmodells.
Aus dem fachlichen Modell des Anwendungsbereichs können zentrale fachliche Klassen der Architektur abgeleitet werden (beispielsweise die Klassen Student, Prüfung und Dozent). Diese müssen vervollständigt werden, etwa durch Attribute, Parameter für die Methoden etc. Die fachlichen Klassen müssen um weitere Klassen ergänzt werden, z. B. solche, die die Prüfungen oder die Studenten verwalten. Weiterhin werden Klassen benötigt, um die Software auf Basis einer gewählten Plattform zu implementieren (z. B. J2EE, Bedienoberfläche auf Basis JFC/Swing). Die technischen Klassen sollten aber von den fachlichen Klassen getrennt werden, da sich technische und fachliche Klassen unterschiedlich schnell weiterentwickeln (Siedersleben, 2004; vgl. auch die ideale Technologie, Abschnitt 16.7.2).
Abb. 17–12 Fachliche Funktionen, modelliert mit Use Cases
Abb. 17–13 Fachliches Begriffsmodell
Die in Abschnitt 17.3 vorgestellten Entwurfsprinzipien sind universell nutzbar, also auch bei einem objektorientierten Entwurf. Ein weiteres wichtiges Entwurfsprinzip ist das Offen-geschlossen-Prinzip; es kann faktisch aber nur bei einer objektorientierten Vorgehensweise angewendet werden.
Wer eine Komponente verwenden möchte, obwohl sie nicht exakt den Anforderungen entspricht, muss sie anpassen. Eine Komponente, die diese Veränderung zulässt, heißt (nach Bertrand Meyer, 1997) offen.
Wer eine Komponente bereits verwendet, ist umgekehrt vor allem daran interessiert, dass sich die Schnittstelle und die Funktion der angebotenen Operationen nicht mehr verändern; denn jede Änderung muss in den Kundenkomponenten nachvollzogen werden. Das ist nicht nur kostspielig, sondern auch extrem fehlerträchtig. Meyer nennt ein solche Komponente geschlossen. Wir streben darum an, Komponenten so zu entwerfen, dass sie sowohl geschlossen, also stabil benutzbar, als auch offen für Erweiterungen sind.
Offensichtlich sind diese beiden Eigenschaften nicht miteinander vereinbar, wenn imperative Programmiersprachen zur Codierung verwendet werden. Verwenden wir jedoch eine objektorientierte Programmiersprache, dann ist es dank der Vererbung möglich, beide Ziele zu erreichen (siehe Abschnitt 17.4.5).
Betrachten wir dazu das folgende Beispiel (siehe Abb. 17–14): Die Klasse A wurde so entworfen, dass sie die Bedürfnisse der bekannten Kundenklassen B, C und D abdeckt. Im Zuge der Weiterentwicklung des Programms werden zwei neue Klassen E und F entworfen. Sie könnten ebenfalls die Klasse A benutzen, wenn diese einige zusätzliche neue Funktionen anböte. Damit die existierenden Kundenklassen der Klasse A nicht von dieser Modifikation betroffen sind, leiten wir von A eine neue Klasse A’ ab. A’ hat alle Merkmale von A und zusätzlich die von E und F benötigten Funktionen.
Um Entwürfe zu erstellen, die dem Offen-geschlossen-Prinzip genügen, müssen die richtigen Abstraktionen gefunden und die veränderbaren und erweiterbaren Aspekte explizit modelliert werden. Die verwendeten Wörter (»offen«, »geschlossen«) sind unglücklich gewählt, denn sie deuten einen Gegensatz an, der nicht besteht. Sinnvoller wäre ein Begriffspaar wie »vollständig« und »erweiterbar«.
Abb. 17–14 Offen-geschlossen-Konstruktion
In gängigen objektorientierten Programmiersprachen wie JAVA oder C++ kann man die beschriebenen Modellierungskonzepte fast eins zu eins umsetzen. Klassen und abstrakte Klassen sind syntaktische Einheiten, Methoden und Attribute können im Sinne des Information Hiding programmiert werden. Die BenutztBeziehung zwischen Objekten wird durch eine Objektreferenz umgesetzt. Um eine Teile-Ganzes-Beziehung zu implementieren, wird typischerweise ein Container-Objekt (z. B. ein Feld oder eine Liste) verwendet, um die Bestandteile zu verwalten.
Die Generalisierungsbeziehung zwischen Klassen kann mit Hilfe der Vererbung direkt im Code realisiert werden. Die Vererbung kann aber auch für andere Zwecke genutzt werden. Meyer (1997) beschreibt viele sinnvolle, aber auch einige zweifelhafte Anwendungen der Vererbung. Wir empfehlen, die Vererbung als Generalisierungsbeziehung (»is a«) zu deuten und sie gemäß dem Ersetzbarkeitsprinzip (substitution principle) zur Bildung von Subtypen zu verwenden, wie es von Liskov und Guttag (2001) definiert wurde.
Zum Wort »Vererbung« ist anzumerken, dass es einen einfachen, in der Gesellschaft seit Jahrtausenden praktizierten Vorgang suggeriert, nämlich, dass Güter von Generation zu Generation weitergegeben werden. Dieses Bild ist irreführend, denn in der objektorientierten Programmierung haben wir bei der Vererbung keinen einmaligen Vorgang, sondern eine dauerhafte Beziehung vor uns, wie wir sie aus der biologischen (genetischen) Vererbung kennen. Aber auch diese Analogie stimmt nicht: Wer die große Nase des Vaters geerbt hat, steht dadurch nicht permanent mit seinem Vater in Verbindung und profitiert auch nicht von einer Schönheitsoperation seines Vorfahren. Vererbung ist also eine höchst ungeeignete, irreführende Metapher.
Der objektorientierte Entwurf hat sich in der Praxis in sehr vielen Bereichen bewährt. Allerdings können wir nicht immer objektorientiert entwerfen. Dies hat im Wesentlichen die folgenden zwei Gründe:
Weil die Elemente des objektorientierten Entwurfs eng an die Konzepte der objektorientierten Programmierung angelehnt sind, fehlen jenseits von Klassen Entwurfsbausteine (und damit Abstraktionen) und Beziehungen (z. B. Subsysteme, Schichten oder Kommunikationsprotokolle). Gerade diese aber brauchen wir, um auf der Systemebene die Architektur zu entwerfen. Die Gliederung einer Software auf dieser Ebene erfolgt oft auch unter funktionalen Gesichtspunkten.
Weil der objektorientierte Entwurf ein allgemeines Entwurfsverfahren ist, kann er nicht in Anwendungsbereichen eingesetzt werden, bei denen die domänenspezifischen Randbedingungen und Anforderungen explizit berücksichtigt werden müssen. Hier müssen domänenspezifische Entwurfsverfahren verwendet werden.
So wurden beispielsweise für den Bereich der eingebetteten Systeme spezielle Entwurfsverfahren und Notationen entwickelt, um die Eigenheiten dieser Systeme explizit zu modellieren. Ein Beispiel ist die Sprache AADL (Architecture Analysis and Design Language), die von der Aerospace Avionic Systems Division der Society of Automotive Engineers für die Spezifikation und den Entwurf von sicherheitskritischen Systemen standardisiert wurde (Feiler, Gluch, Hudak, 2006). Sie führt spezielle Entwurfsbausteine und Beziehungen ein wie Prozesse, Threads, Daten und Subprogramme.
Zusammenfassend lässt sich feststellen: Der objektorientierte Entwurf hat sich bewährt; auf der Ebene des Komponentenentwurfs können wir häufig objektorientiert entwerfen. Die entstandenen Klassenstrukturen können dann in einer objektorientierten Programmiersprache ohne Strukturbrüche codiert werden.
Die Wiederverwendung war und ist bei der Entwicklung von Software ein lohnendes Ziel. Im Laufe der letzten Jahrzehnte ist ein Fundus von Entwurfswissen entstanden, das herangezogen werden sollte, wenn Software-Architekturen entworfen werden. In diesem Abschnitt stellen wir einige wichtige Konzepte vor, die im Kontext des Software-Entwurfs wiederverwendet werden können. Eine detaillierte Betrachtung zum Thema Wiederverwendung findet sich in Kapitel 24.
Wenn sich bestimmte Lösungsstrukturen bewährt haben, kann man sie dokumentieren und später erneut verwenden. Der Architektur-Theoretiker Christopher Alexander hat solche Muster in einem Buch zusammengestellt (Alexander et al., 1977). Dieses Konzept wurde auf die Software übertragen, wir sprechen ebenfalls von Architekturmustern. Sie sind auf der Ebene der Systemarchitektur definiert und stellen Vorlagen dar, nach denen konkrete Architekturen entworfen werden können. Buschmann et al. definieren:
An architectural pattern expresses a fundamental structural organization schema for software systems. It provides a set of predefined subsystems, specifies their responsibilities, and includes rules and guidelines for organizing the relationships between them.
Buschmann et al. (1996)
Die Verwendung eines Architekturmusters impliziert bestimmte Merkmale der Software (Aufbau, Erweiterbarkeit, Kommunikation etc.). Wenn wir ein Architekturmuster verwenden, legen wir damit die Strukturen auf der obersten Ebene der Architektur fest. Die Wahl eines Architekturmusters ist darum eine zentrale Entwurfsentscheidung. Ob das vorgesehene Architekturmuster geeignet ist, hängt davon ab, ob seine speziellen Randbedingungen und Voraussetzungen erfüllt sind.
Shaw und Garlan (1996) sowie Buschmann et al. (1996) beschreiben detailliert einen Katalog von Architekturmustern. Wir beschränken uns hier darauf, einige häufig anwendbare Architekturmuster vorzustellen.
In seiner Arbeit »The Structure of the THE Multiprogramming System« nutzt Edsger W. Dijkstra Software-Schichten, um ein Betriebssystem übersichtlich zu strukturieren (Dijkstra, 1968a). Er zerlegt die Funktionalität des Betriebssystems in fünf Schichten, wobei jede Schicht einen virtuellen Rechner (virtual machine) bildet, der der darüberliegenden Schicht definierte Dienste in gekapselter Form zur Verfügung stellt.
Schichten werden nach den folgenden Regeln festgelegt:
Eine Schicht fasst logisch zusammengehörende Komponenten zusammen.
Eine Schicht stellt Dienstleistungen zur Verfügung, die an der (oberen) Schnittstelle der Schicht angeboten werden.
Die Dienstleistungen einer Schicht können nur von Komponenten der direkt darüberliegenden Schicht benutzt werden.
Schichten bauen also aufeinander auf, ein Durchgriff durch mehrere Schichten ist nicht erlaubt (siehe Abb. 17–15). Ein schichtenbasierter Entwurf hat folgende Vorteile:
Schichten sind nur gekoppelt, wenn sie benachbart sind. Aber auch diese Kopplung ist durch die Kapselung der Operationen gering. Änderungen wirken sich darum meist nur lokal (innerhalb einer Schicht) aus.
Eine Schicht kann aus mehreren entkoppelten Teilen mit hohem Zusammenhalt bestehen; beispielsweise gibt es in einer Schicht, die die Ein- und Ausgabe implementiert, möglicherweise einen Teil für die Bildschirmausgabe, einen anderen für die Sprachausgabe. Änderungen betreffen dann in der Regel nur einen einzigen Teil.
Abb. 17–15 Schema einer Schichtenarchitektur (dargestellt durch UML-Pakete)
Buschmann et al. definieren dieses Architekturmuster wie folgt:
The Layers architectural pattern helps to structure applications that can be decomposed into groups of subtasks in which each group of subtasks is at a particular level of abstraction.
Buschmann et al. (1996)
Züllighoven (2005) bezeichnet Schichten, die nur mit ihren Nachbarschichten kommunizieren, als protokollbasierte Schichten. Eine solche Schicht verbirgt die unter ihr liegenden Schichten und definiert ein Protokoll, das (nur) der darüberliegenden Schicht zur Verfügung steht.
Soll jedoch eine tiefere Schicht für alle höheren Schichten sichtbar sein, damit dort die in der tieferen Schicht enthaltenen Komponenten benutzt oder erweitert werden können, dann wird diese Schicht als objektorientierte Schicht bezeichnet. Objektorientierte Schichten enthalten Klassen, Klassenbibliotheken oder Rahmenwerke. Um aus einer darüberliegenden Schicht auf eine objektorientierte Schicht zuzugreifen, werden die Vererbung und die Benutzt-Beziehung verwendet. Beim Entwurf großer objektorientierter Anwendungen können beide Schichtenarten vorkommen, wobei folgende Regeln zu beachten sind:
Die Komponenten einer Schicht dürfen immer nur Komponenten der nächsttieferen protokollbasierten Schicht benutzen.
Die Komponenten einer Schicht können die Komponenten aller tieferen objektorientierten Schichten nutzen.
Viele interaktive Software-Systeme sind aus den folgenden drei protokollbasierten Schichten aufgebaut: Datenhaltungs-, Anwendungs- und Präsentationsschicht (siehe Abb. 17–16). Dieser Aufbau ist ebenfalls ein Architekturmuster. Er spiegelt in seiner Struktur die grundsätzlichen Aufgaben wider, die bei interaktiven Software-Systemen gelöst werden müssen. Die Präsentationsschicht realisiert die Bedienoberfläche; sie stellt Informationen dar und steuert die Interaktion des Benutzers mit dem System. Dazu kommuniziert die Präsentationsschicht mit der Anwendungsschicht.
In der Anwendungsschicht sind alle Komponenten zusammengefasst, die die fachliche Funktionalität realisieren. Sie sind so konstruiert, dass sie keine Information über die Präsentation enthalten. Um Daten zu manipulieren oder an die Präsentationsschicht weiterzuleiten, greifen die Komponenten der Anwendungsschicht auf die Datenhaltungsschicht zu. Diese sorgt dafür, dass die Daten dauerhaft (persistent) gespeichert werden, meist in einer Datenbank.
Wie die Daten geladen und gespeichert werden, ist ausschließlich der Datenhaltungsschicht bekannt; den anderen Schichten ist diese Information verborgen.
Ist eine Anwendung in protokollbasierte Schichten gegliedert, so lässt sie sich relativ leicht auf verschiedene Rechner verteilen, wie das Beispiel in Abbildung 17–16 zeigt.
Die Verbindung der Verarbeitungsfunktionen durch sogenannte Pipes gehört zu den Charakteristika des Betriebssystems UNIX. Eine Pipe verbindet zwei Verarbeitungsschritte so miteinander, dass das Ergebnis des ersten Schritts im folgenden Schritt als Eingabe verwendet wird. Dieses Konstruktionsmuster kann man für Anwendungen wie folgt verallgemeinern:
Abb. 17–16 Drei-Schichten-Architektur mit physischer Verteilung
Das Pipe-Filter-Architekturmuster dient dazu, eine Anwendung zu strukturieren, die Daten auf einem virtuellen Fließband verarbeitet.
Die einzelnen Verarbeitungsschritte werden in den sogenannten Filtern realisiert. Ein Filter verbraucht und erzeugt Daten. Pipes leiten die Ergebnisse des Filters an die nachfolgenden Filter weiter. Das erste Filter bekommt seine Daten aus der Datenquelle, das letzte liefert sie an die Datensenke. Soll eine Anwendung als Pipe-Filter-Architektur konstruiert werden, dann muss man zuerst die einzelnen Verarbeitungsschritte identifizieren. Anschließend sind Datenformate für je zwei durch eine Pipe verbundene Filter festzulegen. Kann man ein einheitliches Datenformat für alle Filter festlegen, dann können diese auch leicht rekombiniert werden.
Wie das UNIX-Betriebssystem arbeiten auch viele Übersetzer nach dem Pipe-Filter-Architekturmuster. Die Verarbeitungsschritte, also die Filter, sind die lexikalische Analyse, die Syntaxanalyse, die semantische Analyse und die Code-Erzeugung (siehe Abb. 17–17).
Dieses Architekturmuster hat die folgenden Vorteile:
Ein existierendes Filter kann einfach durch eine neue Komponente ersetzt werden, da es relativ einfache Schnittstellen hat (Ein- und Ausgang).
Abb. 17–17 Pipe-Filter-Architektur eines Hochsprachenübersetzers
Nutzen mehrere Filter das gleiche Datenaustauschformat (z.B. ASCII-Dateien), dann können sie durch Rekombination neue Verarbeitungseinheiten realisieren. Diese Eigenschaft kann beispielsweise genutzt werden, um Systemprototypen zu erstellen.
Dem stehen die folgenden Nachteile gegenüber:
Wird aus Gründen der Flexibilität ein einheitliches Datenaustauschformat definiert, dann müssen die einzelnen Filter eventuell das Format anpassen, also Datenkonversionen durchführen. Dies kann die Effizienz merklich beeinträchtigen.
Die Filter benutzen keine globalen Daten. Um trotzdem systematisch mit Fehlersituationen umzugehen, wird eine gemeinsame Strategie für alle Filter benötigt. Diese muss festlegen, wie die Daten im Fehlerfall weiter verarbeitet werden sollen. Im schlimmsten Fall muss die Verarbeitung abgebrochen und vollständig wiederholt werden.
Die erste architektonisch saubere Trennung von Interaktion und Funktion wurde beim Bau der SMALLTALK-Entwicklungsumgebung umgesetzt und als Model-View-Controller-Paradigma (MVC) bezeichnet (Krasner, Pope, 1988). Nach der in Abschnitt 17.2 eingeführten Terminologie ist MVC ein Architekturmuster.
MVC gliedert eine interaktive Anwendung in drei Komponenten:
Das Model3 realisiert die fachliche Funktionalität der Anwendung. Es kapselt die Daten der Anwendung, stellt jedoch Zugriffsoperationen zur Verfügung, mit denen die Daten abgefragt und verändert werden können.
Eine View (Ansicht) präsentiert dem Benutzer die Daten. Sie verwendet die Zugriffsoperationen des Models. Zu einem Model kann es für unterschiedliche Darstellungen derselben Daten beliebig viele Views geben.
Abb. 17–18 Interaktion zwischen den MVC-Komponenten
Jeder View ist ein Controller zugeordnet. Dieser empfängt die Eingaben des Benutzers und reagiert darauf. Muss nach der Interaktion des Benutzers die Visualisierung geändert werden (z. B. nach der Selektion eines Textes), dann informiert der Controller die Views entsprechend. Wählt der Benutzer – beispielsweise über einen Menüeintrag – eine Funktion der Anwendung aus, dann ruft der Controller diese beim Model auf.
Abbildung 17–18 zeigt, wie die drei Komponenten miteinander verbunden sind: View und Model sind allen Komponenten bekannt, der Controller ist es nicht. Das MVC-Architekturmuster führt zu einer losen Kopplung zwischen den Komponenten und setzt das Entwurfsprinzip der Trennung von Zuständigkeiten um.
Wenn die Daten der Anwendung durch die Interaktion des Benutzers verändert werden, müssen die betroffenen Views darauf reagieren. Dazu sieht MVC einen Mechanismus vor, mit dem Veränderungen des Models den Views mitgeteilt werden. Das Model verwaltet dazu eine »Kundenliste« (Register), in der sich alle Views, die das Model darstellen, eintragen müssen. Ändert sich der Zustand des Models, so werden alle registrierten Views informiert; sie können dann reagieren und ihre Darstellung aktualisieren. Dieser Ablauf wird auch als Change-update-Mechanismus bezeichnet.
Abbildung 17–19 zeigt eine objektorientierte Modellierung der MVC-Komponenten. Sie sind durch Objektattribute miteinander verbunden. Das Model bietet neben den anwendungsspezifischen Operationen auch Operationen an, mit denen sich Views an- und abmelden können. Die private Operation propagiere-Aenderung dient dazu, allen Views eine Zustandsänderung des Models mitzuteilen (sie zu propagieren).
Abbildung 17–20 zeigt, welche Nachrichten zwischen den MVC-Komponenten verschickt werden, wenn sich der Zustand des Models ändert.
Abb. 17–19 Objektorientiertes MVC-Modell
Abb. 17–20 Nachrichtenfluss beim Change-update-Mechanismus (Sequenzdiagramm)
Das MVC-Architekturmuster bietet die folgenden Vorteile:
Es ist möglich, für dasselbe Model mehrere View-Controller-Paare vorzusehen. Auf Grund der sauberen Entkopplung vom Model können View-Controller-Paare sogar zur Laufzeit hinzugefügt oder weggenommen werden.
Der Change-update-Mechanismus führt dazu, dass das Model in allen Views immer aktuell visualisiert wird.
Das Architekturmuster hat aber auch Schwächen. Wenn sich die Daten, die das Model verwaltet, sehr oft und sehr schnell ändern, kann das dazu führen, dass die Views diese Veränderungen nicht mehr schnell genug anzeigen können, weil jedes Mal die Daten vom Model erfragt werden müssen.
In Zeiten der imperativen Programmierung waren Software-Systeme abgeschlossen, d. h., jede Erweiterung war eine aufwändige, fehlerträchtige Wartungsarbeit und nur denen möglich, die Zugang zum Quellcode, zur Dokumentation und ggf. zu bestimmten Entwicklungssystemen hatten. Das Plug-in-Architekturmuster bietet allen Benutzern die Möglichkeit, ein System an dafür vorgesehenen Punkten zu erweitern, ohne es zu modifizieren; es setzt somit das Offen-geschlossen-Prinzip auf der Ebene von Anwendungen um.
Eine Anwendung, die nach diesem Architekturmuster aufgebaut ist, besteht aus einem Kern (auch Host = Wirt genannt), der durch sogenannte Plug-ins um neue Funktionen erweitert werden kann. Der Host definiert spezielle Schnittstellen, die sogenannten Erweiterungspunkte, auf die ein Plug-in Bezug nehmen kann (Abb. 17–21).
Abb. 17–21 Schema einer Plug-in-Architektur
Damit ein Plug-in im Kontext des Hosts ausgeführt werden kann, muss es gemäß den vom Host vorgegebenen technischen Konventionen realisiert sein, der Code muss entsprechend abgelegt und das Plug-in beim Host registriert sein. Beim Start des Hosts werden die aktuell vorhandenen Plug-ins identifiziert und entweder sofort oder nach Bedarf geladen. Das Plug-in kann selbst ein Host sein, d. h., Plug-ins können Erweiterungspunkte für weitere Plug-ins definieren.
Eclipse ist eine Anwendung, die konsequent dieses Muster umsetzt (siehe Eclipse, o.J. und Abschnitt 15.3.1); die Werkzeuge der Mozilla-Suite sind weitere Beispiele (Mozilla, o.J.).
Architekturmuster sind Muster auf der Ebene der Systemarchitektur. Ähnliche Muster gibt es auch für den Entwurf der Komponenten; sie werden Entwurfsmuster (design patterns) genannt. Entwurfsmuster gehen auf die Arbeit von Erich Gamma zurück, der die Architektur eines Rahmenwerks für Editoren (ET++) durch einen Katalog von Entwurfsmustern beschreibt (Gamma, 1992). Daraus und aus den Entwurfserfahrungen anderer entstanden zwanzig Entwurfsmuster (Gamma et al., 1995). Auch andere Autoren haben Kataloge mit Entwurfsmustern vorgelegt (z. B. Buschmann et al., 1996).
Entwurfsmuster befassen sich im Gegensatz zu Architekturmustern mit kleineren Einheiten. Sie bieten Lösungen für gängige Entwurfsprobleme auf der Ebene des Feinentwurfs von Komponenten an. Architektur- und Entwurfsmuster dienen dazu, das Wissen über guten Software-Entwurf zu bewahren und wiederholt zu verwenden. Entwurfsmuster werden unabhängig von einer bestimmten Programmiersprache formuliert, sie greifen aber meist auf objektorientierte Konzepte zurück, implizieren damit also eine objektorientierte Codierung. Beispiele für Entwurfsmuster sind das Beobachter- und das Adapter-Muster.
Die Väter der Entwurfsmuster, Gamma, Helm, Johnson und Vlissides, charakterisieren Entwurfsmuster wie folgt:
Design patterns ... are descriptions of communicating objects and classes that are customized to solve a general design problem in a particular context. A design pattern names, abstracts, and identifies the key aspects of a common design structure that make it useful for creating a reusable object-oriented design.
Gamma et al. (1995)
Wir lehnen uns an dieses Verständnis von Entwurfsmustern an und definieren:
Ein Entwurfsmuster beschreibt das Schema einer Lösung für ein bestimmtes, in verschiedenen konkreten Zusammenhängen wiederkehrendes Entwurfsproblem.
Ein Entwurfsmuster bietet eine generische Lösung für ein häufig auftretendes Entwurfsproblem an. Um es zu verwenden, muss man wissen, welches Problem das ist und wie man das Entwurfsmuster im Kontext einer konkreten Architektur ausprägen kann. Die Urheber eines Entwurfsmusters müssen diese beiden Informationen mitliefern.
Bevor wir einige wichtige Entwurfsmuster vorstellen, wollen wir an einem Beispiel zeigen, wie ein Entwurfsmuster eingesetzt werden kann.
Im folgenden Beispiel, übernommen aus Shalloway und Trott (2002), soll eine Anwendung zur elektronischen Verkaufsabwicklung realisiert werden. Im ersten Entwurf sehen wir dafür zwei Klassen vor (Abb. 17–22): Die Klasse Auftragsmanagement dient dazu, eingehende Aufträge (zunächst nur aus Deutschland) anzunehmen, zu prüfen und zu verwalten. Die Aufträge werden an Objekte der Klasse Auftragsabwicklung übergeben, die die Aufträge ausführen. Um die anfallenden Steuern zu ermitteln, enthält diese Klasse die Methode berechneSteuer.
Abb. 17–22 Auftragsabwicklungssystem – Architektur A
Die Anwendung soll nun erweitert werden, damit auch Aufträge aus Österreich und der Schweiz abgewickelt werden können. Die naheliegende Implementierung dieser neuen Anforderung zeigt die Architektur B in Abbildung 17–23.
Abb. 17–23 Auftragsabwicklungssystem – Architektur B
Wir haben von der – jetzt abstrakten – Klasse Auftragsabwicklung drei länderspezifische Klassen abgeleitet, damit in diesen Klassen die geerbte Methode berechneSteuer entsprechend den jeweiligen Vorschriften zur Steuerberechnung realisiert wird. Wenn noch weitere Länder hinzukommen, kann die Klassenhierarchie erweitert werden. Das Auftragsmanagement entscheidet je nach Herkunftsland des Auftrags, welchem Objekt dieser Auftrag zur Abwicklung übermittelt werden muss.
Die Hierarchie der Klassen zur Auftragsabwicklung in Architektur B haben wir nur gebildet, weil die Steuerberechnung von Land zu Land variiert. Wir können diesen variablen Aspekt aber auch explizit durch eine eigene Klasse (SteuerRechner) modellieren, die von der Klasse Auftragsabwicklung benutzt wird. Die verschiedenen Steuerberechnungsvorschriften werden in Unterklassen der Klasse SteuerRechner realisiert. Die Auftragsabwicklungsobjekte können dann die Steuerberechnung an die speziell dafür vorgesehenen Steuerberechnungsobjekte delegieren. Abbildung 17–24 zeigt das Klassendiagramm der neuen Architektur C.
Die Architektur C hat den Vorteil, dass die Steuerberechnungen nach dem Prinzip der Trennung von Zuständigkeiten separat modelliert werden. Wenn sie sich ändern, können die notwendigen Anpassungen lokal durchgeführt werden. Die Klassen dieser Architektur haben zudem einen besseren Zusammenhalt, da Auftragsabwicklung und Steuerberechnung nicht in einer Klasse gemischt werden.
In Architektur C ist das Entwurfsmuster Strategie eingesetzt. Entwurfsmuster beschreiben eine Problemlösung, indem sie angeben, welche Rollen die beteiligten Klassen übernehmen, wofür sie verantwortlich sind und wie sie interagieren.
Abb. 17–24 Auftragsabwicklungssystem – Architektur C
Zur Darstellung der Entwurfsmuster werden Text sowie Klassen- oder Objektdiagramme verwendet. Tabelle 17–3 beschreibt das Strategie-Muster.
Strategie (Strategy) |
|
Problem |
Verwandte Klassen unterscheiden sich lediglich dadurch, dass sie gleiche Aufgaben teilweise durch verschiedene Algorithmen lösen. |
Lösung |
Die Klassen werden nicht – wie üblich – in einer Vererbungshierarchie angeordnet. Stattdessen wird eine Klasse erstellt, die alle gemeinsamen Operationen definiert (StrategyContext). Die Signaturen der Operationen, die unterschiedlich zu implementieren sind, werden in einer weiteren Klasse zusammengefasst (Strategy). Die Rolle Strategy legt somit fest, über welche Schnittstelle die verschiedenen Algorithmen genutzt werden. Von dieser Klasse wird für jede Implementierungs-alternative eine konkrete Unterklasse abgeleitet (ConcreteStrategy). Die Klasse mit der Rolle StrategyContext benutzt konkrete Strategy-Objekte, um die unterschiedlich implementierten Operationen per Delegation auszuführen. |
Struktur |
Tab. 17–3 Das Entwurfsmuster »Strategie«
In unserem Beispiel (Abb. 17–25) kapselt die Klasse SteuerRechner den Aspekt der Steuerberechnung. Die verschiedenen länderspezifischen Vorschriften (Strategien) werden in Unterklassen implementiert. Objekte dieser Klassen werden dazu verwendet, die Steuern zu berechnen. Übertragen wir die Struktur des Strategie-Musters auf unseren Entwurf, dann erhalten wir die Zuordnung der Musterrollen zu den Klassen in Abbildung 17–25.
Abb. 17–25 Zuordnung zwischen den Musterrollen und Klassen
Die Klasse Auftragsabwicklung besetzt die Rolle StrategyContext.
Die Klasse SteuerRechner spielt die Rolle Strategy.
Jede der Klassen CH_SteuerRechner, D_SteuerRechner und A_SteuerRechner hat eine Rolle als ConcreteStrategy.
Wie man an diesem Beispiel sieht, verschafft uns der Einsatz eines Entwurfsmusters nicht nur eine erprobte und tragfähige Architektur für unser Problem, sondern auch einen griffigen Namen, um diese Architektur zu benennen. Wenn wir verstanden haben, wie ein Teil der Architektur, der nach dem Strategie-Muster konstruiert ist, aufgebaut ist und wie die einzelnen Klassen zusammenarbeiten, müssen wir nicht mehr alle Einzelheiten ergründen und nachvollziehen. Entwurfsmuster schaffen also auch eine Sprache, um Entwürfe zu kommunizieren und zu dokumentieren.
Nachfolgend stellen wir einige der in Gamma et al. (1995) beschriebenen Muster vor, die häufig beim Software-Entwurf genutzt werden können. Wir ordnen sie nach Einsatzbereichen, beschreiben für jedes Muster kurz das Problem, das es löst, und geben ein Beispiel an. Für eine detaillierte Beschreibung verweisen wir auf die Originalarbeiten und auf die weiterführende Literatur (z. B. Shalloway, Trott, 2002).
Einzelstück (Singleton) |
|
Problem |
Von einer Klasse darf nur genau ein global verfügbares Objekt erzeugt werden. |
Beispiel |
Es darf nur einen Drucker-Spooler im System geben. |
Memento |
|
Problem |
Der Zustand eines Objekts muss so archiviert werden, dass es wieder in diesen Zustand zurückversetzt werden kann, ohne das Prinzip der Kapselung zu verletzen. |
Beispiel |
Ein Undo-Mechanismus soll realisiert werden. |
Adapter |
|
Problem |
Eine Klasse soll verwendet werden, obwohl ihre Methoden-Signaturen nicht passen und auch nicht verändert werden können. |
Beispiel |
In einem Editor soll eine Klasse, die eine einfache Rechtschreibprüfung realisiert, durch eine zugekaufte Klasse ersetzt werden. |
Vermittler (Mediator) |
|
Problem |
Objekte, die auf komplexe Art miteinander interagieren müssen, dürfen nur lose aneinander gekoppelt sein und müssen sich leicht austauschen lassen. |
Beispiel |
Das Aussehen und der Zustand verschiedener Interaktionsmittel (Menüs, Knöpfe, Eingabefelder) einer grafischen Bedienoberfläche sollen in jedem Interaktionszustand konsistent aufeinander abgestimmt werden. |
Brücke (Bridge) |
|
Problem |
Fachliche Anwendungsklassen und deren Implementierungsvarianten sollen einfach kombiniert und weiterentwickelt werden können. |
Beispiel |
Es sollen Mengen-Klassen implementiert werden (z. B. einfache Menge, Multimenge). Zur Verwaltung der Elemente stehen verschiedene Container-Klassen zur Verfügung (Listen, Bäume etc.). Die Mengen-Klassen bilden die fachlichen Anwendungsklassen; die Container-Klassen sind die Varianten der Implementierung. |
Fassade (Facade) |
|
Problem |
Eine Komponente soll nicht den gesamten, sondern nur einen eingeschränkten Funktionsumfang anderer Komponenten kennen. |
Beispiel |
Eine Komponente zur statistischen Auswertung benötigt nur wenige Funktionen eines Kundeninformationssystems. |
Beobachter (Observer) |
|
Problem |
Mehrere Objekte müssen ihren Zustand anpassen, wenn sich ein bestimmtes Objekt ändert. |
Beispiel |
Alle GUI-Objekte, die ein Dateisystem darstellen, müssen ihre Darstellung anpassen, wenn Dateien eingefügt oder gelöscht werden. |
Abstrakte Fabrik (Abstract Factory) |
|
Problem |
In einer Anwendung müssen kontextabhängig Objekte unterschiedlicher Klassen erzeugt werden. |
Beispiel |
Ein grafischer Editor muss abhängig vom eingesetzten Monitortyp unterschiedliche grafische Objekte erzeugen. |
Schablonenmethode (Template Method) |
|
Problem |
Ein Algorithmus, zu dem es mehrere Implementierungen gibt, soll so realisiert werden, dass man neue Varianten einfach hinzufügen kann. |
Beispiel |
Das Löschen eines Objekts aus einer Objektsammlung kann allgemein beschrieben werden. Je nach Objektsammlung ist dieser Algorithmus jedoch unterschiedlich zu implementieren. |
Strategie (Strategy) |
|
Problem |
Objekte, die sich nur dadurch unterscheiden, dass sie gleiche Aufgaben teilweise durch verschiedene Algorithmen lösen, sollen durch eine Klasse, nicht durch eine Klassenhierarchie implementiert werden. |
Beispiel |
Objekte, die Eingabemasken implementieren, unterscheiden sich darin, wie die Eingaben geprüft werden. |
Zustand (State) |
|
Problem |
Ein Objekt muss sein Verhalten abhängig vom Zustand ändern. |
Beispiel |
Die Berechnung der Mietkosten eines Leihwagens ist davon abhängig, welcher Fahrzeugkategorie der Wagen zugeordnet ist. |
Im Alltag übernimmt eine Person viele Rollen, sie ist beispielsweise Mutter, Tochter, Angestellte, Wählerin, Vereinspräsidentin. Ähnlich verhält es sich mit den Klassen einer Architektur. Jedes einzelne Entwurfsmuster beschreibt ein Lösungsschema für ein bestimmtes Entwurfsproblem. Da es im konkreten Fall eine Reihe solcher Entwurfsprobleme gibt, kann eine Klasse gleichzeitig an mehreren Entwurfsmustern beteiligt sein, d. h., eine Klasse implementiert verschiedene Rollen der einzelnen Muster. Ein solcher musterbasierter Entwurf hilft, das häufig sehr komplexe Zusammenspiel von Klassen zu modellieren und zu verstehen.
Wir wollen dies am Beispiel des JHotDraw-Rahmenwerks zeigen4. Dieses objektorientierte Rahmenwerk definiert und implementiert die gemeinsame Architektur für interaktive grafische Editoren. Es erfüllt unter anderem die folgenden Anforderungen:
Die Reaktion auf eine Benutzerinteraktion (z. B. einen Mausklick in der Zeichenfläche) muss sich leicht ändern lassen.
Eine Grafik soll auf beliebig vielen Zeichenflächen darstellbar sein.
Der Algorithmus, der die Zeichenfläche nach einer grafischen Manipulation aktualisiert, soll einfach austauschbar sein.
Abbildung 17–26 zeigt den Kern des JHotDraw-Klassenentwurfs. Darin kommen die Entwurfsmuster Strategy, Observer, Mediator und State zum Einsatz. Die zentralen Klassen sind DrawingEditor, DrawingView, Drawing und Tool.
Abb. 17–26 Überlagerung der Rollen im Rahmenwerk JHotDraw
Um das komplexe Zusammenspiel dieser Klassen an einer Stelle zu koordinieren und die Klassen voneinander zu entkoppeln, wird das Muster Mediator verwendet. Mediator sieht vor, dass ein Vermittlerobjekt (Mediator) realisiert wird, das die Interaktion aller Objekte kapselt. Jedes an der Interaktion beteiligte Objekt (Colleague) braucht dann nur dieses Vermittlerobjekt zu kennen. Die Klasse DrawingEditor übernimmt die Rolle des Mediators. Die Klassen, deren Zusammenspiel vom Mediator koordiniert wird, also die Klassen DrawingView, Drawing und Tool, haben alle eine Rolle Colleague.
Damit mehrere Zeichenflächen dieselbe Grafik darstellen, werden die entsprechenden Klassen nach dem Muster Observer entworfen. Die Klasse Drawing liefert die beobachteten Objekte, die Zeichnungen. Sie hat im Observer-Muster die Rolle Subject. Die Klasse DrawingView realisiert die Zeichenflächen, die über Änderungen an den Zeichnungen informiert werden. Sie übernimmt damit die Rolle Observer.
Damit der Algorithmus zum Aktualisieren der Zeichenfläche leicht ausgetauscht werden kann, wird er nach dem Muster Strategy separat modelliert. In der Klasse DrawingView wird der Aktualisierungsalgorithmus benötigt, deshalb übernimmt diese Klasse die Rolle StrategyContext. Die Klasse Painter definiert die Schnittstelle des Algorithmus und spielt darum die Rolle Strategy. Ihre Unterklasse SimpleUpdateStrategy implementiert in der Rolle ConcreteStrategy einen einfachen Aktualisierungsalgorithmus.
Die Reaktion eines Editors auf gleiche Benutzerinteraktionen kann unterschiedlich sein. Muss beispielsweise in einem Fall bei einem Mausklick ein neues Symbol erzeugt werden, so muss in einem anderen Fall das ausgewählte Symbol markiert werden. Dieses unterschiedliche Verhalten wird durch das Muster State erzielt. Die Signaturen der Operationen, die je nach Zustand unterschiedliches Verhalten zeigen sollen, werden in eine separate Klasse ausgelagert (State), die die Wurzel einer Klassenhierarchie bildet. In Unterklassen wird das zustandsabhängige Verhalten implementiert (ConcreteState). In der Klasse, die sich zustandsabhängig verhalten soll (StateContext), wird eine Referenz auf die Wurzel dieser Klassenhierarchie angelegt. Je nach Zustand wird dieser Referenz ein konkretes Objekt der Zustandsklassenhierarchie zugewiesen, an das die Ausführung der zustandsabhängigen Operationen delegiert wird.
Im Beispiel werden alle Benutzerinteraktionen, die je nach Zustand des Editors unterschiedlich sein können, in einer eigenen Klasse Tool zusammengefasst, die die Rolle State übernimmt. Ihre Unterklassen, z. B. CreationTool und SelectionTool, realisieren das unterschiedliche Verhalten dadurch, dass sie die gemeinsame Schnittstelle entsprechend redefinieren (ConcreteState). Die Klasse Drawing-View übernimmt die Rolle StateContext, indem sie die Tool-Objekte verwendet, an die die Ausführung der Benutzerinteraktionen delegiert wird.
Eine weiter gehende Betrachtung der Entwurfsmuster in JHotDraw und in anderen Rahmenwerken ist in Riehle (2000) enthalten.
Die Entwicklung von Entwurfsmustern gilt als eine der wichtigsten Innovationen des Software Engineerings in den letzten Jahren. Wir sehen bei ihrem Einsatz die folgenden Vorteile:
Die Entwurfsmuster geben uns die Möglichkeit, die Erfahrungen anderer zu nutzen und erprobte Lösungen einzusetzen.
Sie unterstützen uns, nichtfunktionale Anforderungen, beispielsweise Änderbarkeit oder Wiederverwendbarkeit, beim Architekturentwurf zu berücksichtigen.
Sie schaffen ein Vokabular für den Entwurf und erleichtern dadurch die Dokumentation von Architekturen und die Kommunikation über Architekturen.
Sie können beim Reengineering vorhandener Software als Hilfsmittel zur Analyse dienen.
Sie können flexibel miteinander kombiniert werden. So kann eine Klasse in verschiedenen Rollen an unterschiedlichen Entwurfsmustern beteiligt sein.
Nicht zuletzt helfen die Entwurfsmuster dabei, den Software-Entwurf zu lehren.
Natürlich sind Entwurfsmuster kein Allheilmittel. Manchmal werden sie sogar regelrecht missbraucht; so werden Fehlkonstruktionen wie der übermäßige Gebrauch globaler Datenstrukturen mit der Anwendung eines Entwurfsmusters (in diesem Fall »Singleton«) begründet.
Ein Entwurfsmuster zu verstehen ist einfach. Man braucht jedoch viel Entwurfserfahrung, um die Muster sinnvoll einzusetzen.
Da die Entwicklung von Software teuer ist, liegt es nahe, so viele Komponenten wie möglich mehrfach zu verwenden. Was bereits zuvor eingesetzt wurde, ist in der Regel geprüft, zuverlässig im Einsatz und sofort verfügbar. Die Entwicklungstiefe wird durch die Verwendung vorhandener Bausteine reduziert. Alle diese Aspekte tragen dazu bei, dass Software schneller und günstiger entwickelt werden kann.
Bereits in den Sechzigerjahren wurden Programmbibliotheken (z.B. für Matrixoperationen in FORTRAN) entwickelt und eingesetzt. Allerdings war die Flexibilität dieser Bausteine sehr begrenzt.
Als die Objektorientierung aufkam, war eine ihrer wichtigsten Verheißungen, dass damit die Wiederverwendung signifikant verbessert würde. Der Schlüssel dazu ist die Vererbung zwischen Klassen. Schon frühzeitig wurden Klassenbibliotheken entwickelt, deren Funktionalität bei der Anwendungsentwicklung oft benötigt wird. Vorreiter war hier die SMALLTALK-80-Entwicklungsumgebung, die dem Anwendungsentwickler eine Vielzahl universeller Klassen zur Verfügung stellte. Wir definieren:
Eine Klassenbibliothek besteht aus einer Menge von Klassen, die wiederverwendbar sind und allgemein – also unabhängig vom Anwendungskontext – nutzbare Funktionalität anbieten.
Aus Sicht der Anwendung werden die Klassen einer Bibliothek direkt benutzt, oder die Klassen der Anwendung erben von den Bibliotheksklassen. Klassenbibliotheken setzen somit das Offen-geschlossen-Prinzip um (Abschnitt 17.4.4); sie können von der Anwendung unverändert genutzt, aber auch flexibel erweitert werden.
Abbildung 17–27 zeigt eine Situation, bei der die Anwendung Klassen verschiedener Bibliotheken nutzt. Die DS-Bibliothek stellt allgemein wiederverwendbare Datenstrukturen wie Listen oder Bäume zur Verfügung; die GUI-Bibliothek enthält Klassen, die benutzt werden, um die Bedienoberfläche zu realisieren; die DB-Bibliothek bietet Klassen an, die Daten der Anwendung in einer Datenbank persistent, also über den einzelnen Programmlauf hinaus, speichern können.
Abb. 17–27 Nutzung von Klassenbibliotheken
Klassenbibliotheken sind heute feste Bestandteile der Entwicklungsumgebungen, die zu den objektorientierten Programmiersprachen gehören. Insbesondere für JAVA stehen viele Bibliotheken zur Verfügung.
Wenn immer wieder sehr ähnliche Anwendungen entwickelt werden, sollten dazu nicht nur einzelne Klassenbibliotheken genutzt werden, sondern die Anwendungen sollten auf der Basis einer generischen Lösung entwickelt werden. Eine solche generische Lösung wird auch als Rahmenwerk (Framework) bezeichnet. Es gibt in der Literatur verschiedene Definitionen, wir halten uns an Züllighoven:
Ein Rahmenwerk (Framework) ist eine Architektur aus Klassenhierarchien, die eine allgemeine generische Lösung für ähnliche Probleme in einem bestimmten Kontext vorgibt.
Züllighoven (2005)
Bei einem Rahmenwerk werden nicht nur einzelne Klassen, sondern die gesamte Architektur und die Konstruktion aus kooperierenden Klassen wiederverwendet. Im Gegensatz zu einer Klassenbibliothek gibt ein Rahmenwerk der Anwendung auch den Kontrollfluss vor.
Ein Rahmenwerk hat definierte Schnittstellen, an denen die generische Lösung durch anwendungsspezifischen Code erweitert werden kann (siehe Abb. 17–28). Diese Stellen werden als Hot Spots5 (Pree, 1997) bezeichnet. Die Entwicklung einer Anwendung auf Basis eines Rahmenwerks unterscheidet sich drastisch von der herkömmlichen Anwendungsentwicklung, bei der lediglich Klassenbibliotheken genutzt werden. Während dort der gesamte Kontrollfluss durch den Anwendungsentwickler festgelegt wird, müssen bei der rahmenwerkbasierten Entwicklung die anwendungsspezifischen Teile in das Rahmenwerk und in den darin vorgegebenen Kontrollfluss integriert werden.
Abb. 17–28 Schema einer rahmenwerkbasierten Anwendung
Ein Rahmenwerk kann so konstruiert sein, dass man es ohne Kenntnis seiner Interna verwenden, also zu einer Anwendung erweitern kann. Wir sprechen in diesem Fall von einem geschlossenen Rahmenwerk. Die Alternative ist eine Konstruktion, die vom Entwickler der Anwendung verlangt, dass er die Interna kennt, insbesondere den Code der Hot Spots, da er neuen anwendungsspezifischen Code entwickeln und integrieren muss. In diesem Fall handelt es sich um ein offenes Rahmenwerk. In der Literatur werden dafür ähnlich wie beim Test die Attribute »black box« und »white box« verwendet; während die Metapher der Black Box anschaulich ist, hat die Charakterisierung als White Box keinen Sinn, denn ein weißer Kasten ist weder durchsichtig noch offen. Darum bleiben wir bei »geschlossen« und »offen«.
Um ein offenes Rahmenwerk zu einer Anwendung zu erweitern, muss der Entwickler die Architektur, den vorgegebenen Kontrollfluss und die im Rahmenwerk realisierten Mechanismen sehr genau kennen. Technisch gesehen wird ein offenes Rahmenwerk dadurch erweitert, dass die – häufig abstrakten – Klassen der Hot Spots spezialisiert werden. Das erfordert einiges Wissen darüber, welche Klassen zu welchem Zweck spezialisiert und welche Methoden redefiniert werden müssen. In der Regel kennt der Entwickler das Rahmenwerk und die darin realisierten Mechanismen und Abläufe aber nicht genau. Eine entsprechende Dokumentation, die alle Erweiterungsmöglichkeiten vollständig beschreibt, wäre umfangreich und aufwändig. Darum gehören zu offenen Rahmenwerken neben einer Dokumentation der zentralen Entwurfsentscheidungen und Komponenten der Architektur auch Beispielanwendungen, die der Entwickler analysieren und als Vorlagen nutzen kann, um seine eigene Anwendung zu schaffen.
In der Praxis entsteht ein geschlossenes Rahmenwerk aus einem offenen, nachdem damit ausreichende Erfahrungen gesammelt wurden. Dann können Bereiche identifiziert werden, die in geschlossener Form zur Anwendungsentwicklung verwendbar sind.
Ein geschlossenes Rahmenwerk erweitert der Entwickler, indem er Objekte spezieller Rahmenwerksklassen erzeugt und konfiguriert, auf die dann das Rahmenwerk zugreift. Dazu können auch Werkzeuge angeboten werden, mit denen das Rahmenwerk erweitert und konfiguriert wird. Wenn die Umstellung vom offenen zum geschlossenen Rahmenwerk nur teilweise vollzogen ist, wenn also bestimmte Hot Spots durch Unterklassenbildung, andere durch eine werkzeuggestützte Konfiguration erweitert werden (siehe beispielsweise Lichter, Schneider, 1993), spricht man auch von einer »Grey Box«.
Um ein Rahmenwerk zu entwickeln, muss man sein Anwendungsgebiet sehr gut kennen. Technisch ist anzustreben, dass sich die anwendungsspezifischen Erweiterungen möglichst sauber in das Rahmenwerk integrieren lassen. Beim Architekturentwurf von Rahmenwerken werden deshalb an den Integrationsstellen häufiger als sonst üblich Entwurfsmuster eingesetzt, insbesondere solche, die es erlauben, universelle Merkmale von variablen, in diesem Sinne anwendungsspezifischen Merkmalen sauber zu trennen.
Bis ein Rahmenwerk wirklich brauchbar ist, wird viel Aufwand investiert. Darum muss sein Nutzen entsprechend groß sein, sonst lohnt sich die Entwicklung nicht. Wir betrachten diesen Aspekt und andere Aspekte der Wiederverwendung in Kapitel 24.
Beispiele für häufig eingesetzte Rahmenwerke sind:
JWAM
Ein Rahmenwerk zur Konstruktion von Anwendungen nach der Werkzeug-Material-Metapher (JWAM, o.J.).
GEF (Graphical Editing Framework)
Ein in JAVA entwickeltes Rahmenwerk zur Konstruktion grafischer Editoren auf Basis des Eclipse Modeling Frameworks (Moore et al., 2004).
Weitere Beispiele für Rahmenwerke werden in Lewis et al. (1995) sowie in Fayad und Johnson (2000) beschrieben. Es sei an dieser Stelle darauf hingewiesen, dass Rahmenwerke eine naheliegende Implementierungstechnik für Software-Produktlinien sind.
Diese Gruppen stehen zwischen abstrakten Architekturmustern und konkreten Architekturen. Es handelt sich um generische Software-Architekturen, die zwar konkrete Strukturen vorgeben, aber die Produktarchitektur nicht bis ins Detail festlegen, sodass verschiedene Ausprägungen möglich bleiben.
Die Entwicklung von Produktlinien ist ein vielversprechender Ansatz, eine Reihe ähnlicher Produkte, die auf einer einzigen Plattform basieren, kostengünstig und schnell zu entwickeln, indem man den gemeinsamen Kern nur einmal realisiert. Dazu ist es notwendig, die Unterschiede und die Gemeinsamkeiten der Produkte zu identifizieren. Dann wird die Plattform so entwickelt, dass sie die Gemeinsamkeiten aller Produkte enthält und es erlaubt, produktspezifische Ergänzungen systematisch hinzuzufügen. Offensichtlich spielt die Architektur der Plattform eine zentrale Rolle.
Northrop und Clements definieren eine Produktlinienarchitektur wie folgt:
product line architecture — A core asset that is the software architecture for all the products in a software product line. A product line architecture explicitly provides variation mechanisms that support the diversity among the products in the software product line.
Northrop, Clements (2007)
Eine Produktlinienarchitektur ist demnach die Architektur des gemeinsamen Kerns aller Produkte einer Familie. Die Architekturen der einzelnen Produkte werden auf Basis der Produktlinienarchitektur entworfen; an definierten Stellen sind sie individuell ausgeprägt.
Um eine Produktlinienarchitektur produktspezifisch zu erweitern, können verschiedene Mechanismen verwendet werden.
In Unterklassen (also mit Hilfe der Vererbung) werden notwendige Erweiterungen implementiert oder Standardlösungen des Kerns überschrieben.
Es werden Erweiterungspunkte definiert, wo produktspezifische Teile an die Plattform angedockt werden können.
Aus einer Reihe vorgefertigter Komponenten werden diejenigen ausgewählt, die zu einem speziellen Produkt gehören. Dieser Schritt wird als (Produkt-) Konfiguration bezeichnet.
Technisch bietet es sich an, Produktlinienarchitekturen durch Rahmenwerke oder durch Kombinationen von Komponenten und Rahmenwerken zu realisieren.
Informationen über erfolgreiche Produktlinienentwicklungen finden sich in der sogenannten »Product Line Hall of Fame« (SPLC, o.J.). Die Entwicklung von Produktlinien wird aus organisatorischer und technischer Sicht detailliert z. B. in Pohl, Böckle, van der Linden (2005) vorgestellt.
Eine Referenzarchitektur (auch Standard- oder Modellarchitektur genannt) ist gegenüber der Produktlinienarchitektur, die für eine Menge ähnlicher Produkte gilt, weiter abstrahiert. Eine Referenzarchitektur definiert für einen ganzen Anwendungsbereich, d. h. für alle Software-Systeme dieses Bereichs, eine erprobte und wiederverwendbare Architektur. Es ist offensichtlich, dass eine Referenzarchitektur sehr abstrakt formuliert sein muss und nicht im Kopf eines Forschers entsteht, sondern als Essenz aus den Erfahrungen, die in der Praxis bei einer Reihe ähnlicher Anwendungsentwicklungen gesammelt wurden. In Anlehnung an Beneken (2008) definieren wir:
Eine Referenzarchitektur definiert Software-Bausteine für einen Anwendungsbereich durch ihre Strukturen und Typen, ihre Zuständigkeiten und ihre Interaktionen.
Eine bereits seit langem erprobte Referenzarchitektur existiert für den Übersetzerbau (siehe Abb. 17–17). Ein wesentlich umfangreicheres Beispiel für eine Referenzarchitektur ist die Versicherungsanwendungsarchitektur (VAA) der Deutschen Versicherungswirtschaft (GDV, 2001). Sie spezifiziert versicherungsfachliche Prozesse und Funktionsgruppen, z. B. die Partner- und Vertragsverwaltung, sowie fachliche Datentypen in Form von Geschäftsobjekten.
Nach dem Schwerpunkt unterscheidet Beneken (2008) zwischen funktionaler, logischer und technischer Referenzarchitektur.
Eine funktionale Referenzarchitektur (auch fachliche Referenzarchitektur genannt) modelliert für einen bestimmten Bereich den Funktionsumfang der Anwendungssysteme durch Funktionsgruppen und deren Datenflussbeziehungen.
Abb. 17–29 Funktionale Referenzarchitektur für Portal-Software
Für den Bereich der sogenannten Portal-Software wurde die in Abbildung 17–29 gezeigte Referenzarchitektur vorgeschlagen (Taubner, 2003). Da funktionale Referenzarchitekturen nur die Funktionen modellieren, die allen Anwendungen gemeinsam sind, müssen sie erweiterbar sein. In der Abbildung 17–29 sind beispielhaft behördenspezifische Erweiterungen an dieser Referenzarchitektur grau unterlegt. Wird eine funktionale Referenzarchitektur für eine konkrete Anwendung ausgeprägt, dann kann eine Funktionsgruppe auf mehrere Komponenten aufgeteilt werden. Ebenso kann aber auch eine Komponente mehrere Funktionsgruppen implementieren.
Eine logische Referenzarchitektur liegt zwischen der funktionalen und der technischen Referenzarchitektur. Sie definiert für die Anwendungen des betrachteten Bereichs eine Architektur in Form von Software-Bausteinen (beispielsweise Schichten und Komponenten) und deren Beziehungen. Die Schnittstellen der Software-Bausteine sind zwar technisch motiviert, werden aber typischerweise nur informal beschrieben.
Ein Beispiel für eine logische Referenzarchitektur ist die in Abbildung 17–30 dargestellte Quasar-Referenzarchitektur für betriebliche Informationssysteme (Haft, Humm, Siedersleben, 2005).
Abb. 17–30 Quasar-Referenzarchitektur für Informationssysteme
Die zentrale Komponente ist der Anwendungskern, der die fachliche Logik des Informationssystems realisiert. Dieser nutzt über Schnittstellen Komponenten zur Autorisierung und zum Lesen und Speichern der Daten. Der Anwendungskern selbst wird über eine Fassade benutzt (siehe S. 441), typischerweise von Dialog-, Workflow- und Batch-Komponenten. Diese Referenzarchitektur unterscheidet weiterhin zwischen fachlichen und technischen Komponenten und setzt damit das Prinzip »Trennung von Zuständigkeiten« um. Ein weiteres bekanntes Beispiel einer logischen Referenzarchitektur ist das ISO/OSI-Referenzmodell für Kommunikationssysteme (ISO/IEC 7498-1, 1996).
Eine technische Referenzarchitektur legt die Implementierungstechnologien (z. B. Sprachen, Bibliotheken, Rahmenwerke, Komponenten, Kommunikationsmechanismen) fest, um logische Referenzarchitekturen zu realisieren. Bekannte Beispiele für technische Referenzarchitekturen sind J2EE und .NET.
J2EE ist eine von Sun entwickelte Architektur für komponentenbasierte, mehrschichtige Anwendungen.
.NET fasst eine Reihe von Technologien der Firma Microsoft zusammen, um Web-Anwendungen und Arbeitsplatzanwendungen für das Windows-Betriebssystem auf einer einheitlichen Plattform zu erstellen.
Während dies recht allgemein nutzbare technische Referenzarchitekturen sind, kann JWAM als technische Referenzarchitektur für die Konstruktion von Anwendungen nach dem Werkzeug-Material-Ansatz verstanden werden (Züllighoven, 2005). Abbildung 17–31 zeigt die schichtenbasierte Architektur von JWAM; die Basistechnologie ist JAVA.
Abb. 17–31 JWAM-Referenzarchitektur
Produktlinien- und Referenzarchitekturen können nur auf Basis großer Erfahrungen in der Software-Entwicklung entstehen. Und selbst wenn solche Erfahrungen verfügbar sind, ist es schwierig, tragfähige und anpassbare Referenzarchitekturen zu entwerfen. Verfügt man jedoch über erprobte Referenzarchitekturen, dann ist es fahrlässig, sie zu ignorieren und eigene, neue Architekturen zu entwickeln.
Die Qualität der Architektur bestimmt maßgeblich die Qualität des entwickelten Systems, und zwar dauerhaft. Darum ist es wichtig, die Qualität der Architektur zu prüfen. Da Qualität nicht absolut, sondern nur durch die Anforderungen definiert ist, müssen wir wissen, welche der Anforderungen durch die Architektur umgesetzt werden müssen. Wenn wir diese Anforderungen kennen, können wir feststellen, ob die Architektur geeignet ist, die Anforderungen zu erfüllen.
Bei den Anforderungen an ein System unterscheiden wir funktionale und nichtfunktionale Anforderungen (Abschnitt 16.4.4). Für den Architekturentwurf sind die nichtfunktionalen Anforderungen besonders wichtig, da sie die Freiheit der Konstruktion einschränken. Das gilt vor allem für die Anforderungen an die folgenden Qualitäten:
Testbarkeit
Jede Software muss getestet werden. Wird dies beim Architekturentwurf nicht berücksichtigt, kann man nicht erwarten, dass sich das implementierte System leicht testen lässt. In jedem Fall wird der Test durch eine Architektur, die hohe Lokalität schafft, wesentlich erleichtert, also durch eine Gliederung in abgeschlossene, einzeln testbare Einheiten. Auch spezielle Testschnittstellen tragen zur höheren Testbarkeit bei.
Wartbarkeit, Erweiterbarkeit
Systeme, die eingesetzt werden, müssen dann und wann korrigiert, vor allem aber immer wieder den sich ändernden Anforderungen angepasst werden. Auch wenn man beim Architekturentwurf die Zukunft nicht genau kennt, kann man doch Bereiche des Systems, die sehr wahrscheinlich angepasst oder erweitert werden müssen, so entwerfen, dass der Aufwand dafür akzeptabel ist.
Portierbarkeit
Systeme, die viele Jahre eingesetzt werden, müssen immer wieder auf andere Plattformen (Betriebssysteme) übertragen werden oder Änderungen der umgebenden Software (z. B. Datenbanksysteme, Middleware) nachvollziehen. Solch eine Anpassung wird als Portierung bezeichnet. Die Portierung wird wesentlich erleichtert, wenn die Architektur dafür sorgt, dass die Abhängigkeiten von vorgegebener Software an wenigen Stellen konzentriert sind.
Die Software-Architektur ist nicht Selbstzweck; sie ist gut, wenn sie dazu beiträgt, dass das realisierte System die Anforderungen erfüllt. Jede Bewertung oder Prüfung muss sich an diesem Ziel orientieren. Meist gibt es mehrere mögliche Architekturen; dann müssen diese gegenübergestellt und verglichen werden.
Ist die Architektur (hier insbesondere die statische Struktur) erst einmal gewählt und in Beton oder Code gegossen, dann lässt sie sich nur noch mit sehr hohem Aufwand verändern. Darum sollte die Entscheidung für eine bestimmte Architektur gründlich überprüft und dann erst umgesetzt werden.
Die dynamische Sicht, die Funktion, liefert die Anforderungen an die Statik. Trotzdem kann die Funktion später meist relativ leicht verändert werden. In Amerika sieht man oft Kirchen, die als Restaurants oder Schuhgeschäfte dienen, bei uns werden viele alte Industriebauten als Museen oder Theatersäle genutzt, und manches Schloss beherbergt eine Universität. Es ist das Kennzeichen einer guten Architektur, dass sie solche Änderungen nicht grundsätzlich ausschließt.
Die systematische Prüfung einer Architektur erfolgt am besten durch ein Review (siehe Abschnitt 13.5). Dazu müssen natürlich die Anforderungen und Bewertungskriterien vorher festgelegt sein. Als Gutachter eines Architektur-Reviews kommen nur Fachleute in Frage, die über das notwendige Wissen und über ausreichende Entwurfserfahrung verfügen.
Der Architekturentwurf und seine Bewertung kann durch Prototypen unterstützt werden, die in diesem Falle nicht dazu dienen, die Bedienschnittstelle festzulegen. Vielmehr wird man sich auf unklare und riskante Bereiche konzentrieren. Mit Prototypen können insbesondere Anforderungen an Leistungsaspekte, Skalierbarkeit, Lastverteilung etc. untersucht und bewertet werden.
Schön wäre es, wenn wir die Qualität einer Architektur messen könnten. Es gibt verschiedene Metriken, um Merkmale von Architekturen zu bewerten (z. B. Kopplungs- und Zusammenhaltsmetriken). Leider ist der Nutzen dieser Metriken zweifelhaft, und zudem können sie erst berechnet werden, wenn die Architektur codiert wurde, weil die Messwerkzeuge dazu den Programmcode analysieren müssen. Reißing (2002) hat sich gründlich mit dem Problem, Entwürfe vor der Codierung zu bewerten, auseinandergesetzt.
Es sei noch erwähnt, dass das Software Engineering Institute (SEI) eine Methode zur Bewertung von Architekturen entwickelt hat, die ATAM (Architecture Tradeoff Analysis Method) genannt wird. Diese legt in Form von Phasen und Schritten fest, wie eine Architekturbewertung durchzuführen ist und wer daran beteiligt sein sollte. ATAM wird in Bass, Clements, Kazman (2003) ausführlich vorgestellt.