Zur Webseite der Informatik

Prinzipien für den objektorientierten Entwurf

Grundlagen

Es hat sich in Laufe der Zeit eine Menge Erfahrungswissen darüber angesammelt, welche Eigenschaften eines Entwurfs wünschenswert sind. Aus diesen Erfahrungen haben sich Prinzipien entwickelt, bei deren Beachtung erfahrungsgemäß ein guter Entwurf erreicht werden kann.

Balzert (1985a, S. 2) umreisst den Begriff des Prinzips wie folgt: ,Prinzipien sind Grundsätze, die man seinem Handeln zugrundelegt. Sie sind allgemeingültig, abstrakt, allgemeinster Art. Prinzipien bilden eine theoretische Grundlage. Sie werden aus der Erfahrung und Erkenntnis hergleitet und durch sie bestätigt."

Ross et al. (1975) ist ein Beispiel für eine frühe Sammlung von Prinzipien des Software Engineerings. Die sieben dort betrachteten Prinzipien finden sich fast alle in der folgenden Zusammenstellung wieder. Balzert (1985a, 1985b), Davis (1995) und Buschmann et al. (1996, Kap. 6.3) sind weitere Beispiele für Sammlungen von Prinzipien.

Leider gibt es für die meisten dieser Prinzipien wenig empirische Untersuchungen über ihre Wirksamkeit und ihre Relevanz. Sie stellen eher eine Art Tradition im Software Engineering dar, die durch überwiegend positive Erfahrungen aufrechterhalten wird. Obwohl der empirische Nachweises der Brauchbarkeit fehlt, werden in vielen wissenschaftlichen Arbeiten diese Prinzipien herangezogen, um aus ihnen Entwurfsziele zu extrahieren und dafür dann Metriken abzuleiten. Diese Metriken werden dann zur Entwurfsbewertung verwendet.

Die folgende Sammlung von Prinzipien konzentriert sich auf dem Entwurfsbereich, insbesondere den objektorientierten Entwurf. Es werden strategische und taktische Prinzipien unterschieden.

Strategische Prinzipien

Davis (1995) stellt eine Reihe von Entwurfsprinzipien zusammen, die größtenteils aus der Tradition des strukturierten Entwurfs stammen. Einige davon haben eindeutig strategischen Charakter, andere gehören eher zu den taktischen Entwurfsprinzipien. In diesem Abschnitt enthält eine Auswahl der strategischen Prinzipien aus Davis (1995), ergänzt durch einige andere.

Führe den Entwurf auf die Anforderungen zurück

Es ist wichtig, dass jede Entwurfsentscheidung auf ihre zugehörigen Anforderungen zurückgeführt werden kann (und umgekehrt). Diese Eigenschaft wird als Verfolgbarkeit (traceability) bezeichnet. Sie ist beim Entwurf hilfreich, um die vollständige Abdeckung der Anforderungen prüfen zu können. Außerdem hilft die Verfolgbarkeit bei der Identifikation von Stellen, die geändert werden müssen, wenn sich die Anforderungen ändern oder Fehler auftreten. Glass (1992) schlägt zu diesem Zweck den Aufbau einer Matrix vor, in der die Anforderungen den Entwurfskomponenten gegenübergestellt werden. Ein Eintrag in einer solchen Matrix bedeutet dann, dass eine Entwurfskomponente an der Realisierung einer Anforderung beteiligt ist. Diese Idee geht auf Boehm (1974) zurück, der in einer Anforderungs-/Eigenschaftsmatrix Anforderungen mit angestrebten Systemaspekten verknüpfte und daraus Entwurfsentscheidungen ableitete.

Erfinde nicht das Rad neu

Falls immer es möglich ist, sollte für eine vorgesehene Komponente des Entwurfs eine bereits vorhandene Komponente wiederverwendet werden, statt eine neue zu schaffen. Dies ist im Ingenieurwesen völlig normal, in der Software-Entwicklung aber nicht. Wiederverwendung hat den Vorteil, dass bereits erprobter Code relativ hoher Qualität eingesetzt wird. Die Kosten für die eigene Fertigung einer solchen Komponente mit gleicher Qualität sind in der Regel viel höher. Die Eigenfertigung hat nur den Vorteil, dass die Komponente völlig nach den eigenen Vorstellungen erstellt werden kann, während bei konsequenter Wiederverwendung unter Umständen Kompromisse nötig sind.

Kenne den Anwendungsbereich

Real world constraints always bastardize the most elegant design. However, people tend to focus on these constraints too soon during design activity. They miss out on the potential "big win" that is possible through problem domain understanding.
(Sam Adams, zitiert nach Coad, Yourdon, 1991, S. 148)

Der Entwerfer sollte nicht nur die spezifischen Anforderungen für das Produkt kennen, sondern auch mit dem Anwendungsbereich vertraut sein. Denn von dort gibt es viele weitere implizite Anforderungen, die den Anwendern aber so (unbewusst) selbstverständlich waren, dass sie diese nicht mitgeteilt haben.

Der Entwerfer sollte die Begriffswelt und typische Abläufe der Anwendung kennen, aber auch mit ihrer Arbeitsumgebung vertraut sein. Dazu gehört die technische Umgebung genauso wie die soziale Umgebung, die aus den Benutzern und den geltenden Gesetzen und Standards besteht. Nur dann können die optimale Architektur und geeignete Algorithmen gewählt werden.

Sorge für intellektuelle Kontrolle

Witt et al. (1994) verlangen mit dem Prinzip der intellektuellen Kontrolle (intellectual control; von Dijkstra (1972) auch als intellectual manageability bezeichnet), dass sowohl die Entwickler als auch die Wartungsprogrammier in der Lage sein sollten, den Entwurf vollständig zu verstehen. Dies wird durch hierarchische Strukturierung, Abstraktion und Einfachheit der einzelnen Komponenten erleichtert. Außerdem muss der Entwurf gut dokumentiert sein. Die meisten in See Taktische Prinzipien genannten taktischen Prinzipien dienen der Umsetzung dieses strategischen Prinzips.

Minimiere den intellektueller Abstand

Nach Dijkstra ist intellektueller Abstand der Unterschied (z.B. in der Struktur) zwischen dem Problem und der Software-Lösung. Fairley (1985) stellt fest, dass es leichter ist, eine Software zu warten, wenn der intellektuelle Abstand gering ist. Dazu sollten sich die relevanten Begriffe der Problemwelt (in der Regel die reale Welt) möglichst genau in der Lösung wiederfinden. Dieses Prinzip entspricht dem Prinzip der strukturellen Übereinstimmung (structural correspondence) von Jackson (1975). In der Diskussion der Objektorientierung wird häufig behauptet, dass eine Modellierung der realen Welt mit Objekten ,natürlich" ist, d.h. der intellektuelle Abstand ist naturgemäß gering. Allerdings darf man nicht übersehen, dass jeder Mensch ein anderes Modell der realen Welt besitzt, weshalb der intellektuelle Abstand ein subjektives Maß ist.

Stelle konzeptionelle Integrität her

Das Prinzip der konzeptionellen Integrität (conceptual integrity; Brooks, 1995, Kap. 4) von Witt et al. (1994) fordert, dass der gesamte Entwurf einem einheitlichen Stil folgen soll. Dazu gehört zum Beispiel die Namensgebung, die Verwendung von einheitlichen Mustern zur Lösung ähnlicher Aufgaben (z.B. der Fehlerbehandlung). Ziel ist es, dass der Entwurf am Ende so aussieht, als sei er von einer einzigen Person geschaffen worden. Das erleichtert anderen Entwicklern, sich in den Entwurf einzuarbeiten.

Brooks (1995) stellt fest, dass konzeptuelle Integrität am besten dann erreicht wird, wenn nur einer oder aber wenige Entwickler am Entwurf beteiligt sind. Bass et al. (1998) schlagen die Verwendung von Architektur- und Entwurfsmustern vor, um die konzeptionelle Integrität zu verbessern.

Verberge Entwurfsentscheidungen (Geheimnisprinzip)

Mit dem Geheimnisprinzip (information hiding) fordert Parnas (1972c), Entwurfsentscheidungen, die sich wahrscheinlich ändern, vor dem Rest des Systems zu verbergen. Dies kann zum Beispiel durch Kapselung geschehen: Die Entscheidungen werden in separate Module ausgelagert und dort hinter geeigneten Schnittstellen verborgen. Die Entscheidungen werden so vom Rest des Systems isoliert und hinter einer neutralen Schnittstelle versteckt. Sie können also geändert werden, ohne dass der Rest des Systems betroffen ist. Beispiele für Entwurfsentscheidungen, die verborgen werden sollten, sind der Aufbau von Datenstrukturen und konkrete Algorithmen. Laut Parnas (1972b) hilft das Zurückhalten von Information auch dabei, die Kopplung der Module untereinander zu reduzieren, da weniger Wissen zur Verfügung steht, das zu unnötigen Abhängigkeiten untereinander führen könnte.

Das Geheimnisprinzip ist auch als need-to-know principle bekannt, da der Verwender eines Moduls nur die Informationen bekommen soll, die er für die Verwendung braucht. Diese Informationen sind in Form einer Modulspezifikation verfügbar. Parnas (1972a) charakterisiert die Modulspezifikation folgendermaßen: ,The specification must provide to the intended user all the information that he will need to use the program correctly, and nothing more ."

Minimiere die Komplexität

The art of programming is the art of organizing complexity.
(Edsger W. Dijkstra)

Komplexität ist ein vieldeutiger Begriff, der in der Literatur in den unterschiedlichsten Bedeutungen verwendet wird. Der IEEE Standard 610.12-1990 zum Beispiel definiert Komplexität über die Verständlichkeit und als Gegenteil von Einfachheit:

Definition 2-1 (Complexity, IEEE 610.12-1990)

The degree to which a system or component has a design or implementation that is difficult to understand or verify. Contrast with: simplicity.

Fenton und Pfleeger (1996, S. 245) unterscheiden vier Arten von Komplexität:

  • Problem-Komplexität: die Komplexität des zugrunde liegenden Problems
  • Algorithmische Komplexität: die Komplexität der algorithmischen Lösung des Problems (auch als Effizienz bezeichnet). Hier wird in der Regel Zeit- und Platzkomplexität unterschieden.
  • Strukturelle Komplexität: die Komplexität der Struktur der Lösung. Aspekte der Struktur sind Kontrollfluss, Datenfluss, Datenstrukturen und die Gliederung in Lösungskomponenten (Module).
  • Kognitive Komplexität: der Aufwand, der vom Menschen zum Verständnis der Lösung erbracht werden muss.

Die Definition von IEEE passt hier vor allem zu der kognitiven Komplexität. Die kognitive Komplexität eines Entwurfs sollte gering sein, da er nur dann brauchbar sein, wenn er möglichst verständlich ist. Nach übereinstimmender Meinung vieler Autoren gibt es eine Grundkomplexität, die probleminhärent ist (essential complexity bei Brooks, 1987). Diese muss sich auch in der Lösung wiederfinden. Die darüber hinausgehende Komplexität der Lösung (accidential complexity) sollte dagegen so gering wie möglich ausfallen.

Interessanterweise kann die Komplexität einzelner Bestandteile des Entwurfs reduziert werden, wenn eine höhere Komplexität des Gesamtsystems in Kauf genommen wird (zum Beispiel durch Abstraktion). Bei der Kontrolle von Komplexität geht es nach einem ersten Schritt der Entfernung überflüssiger Komplexität um eine geschickte ,Verteilung" der verbliebenen Komplexität.

Da die kognitive Komplexität subjektiv und schwer zu ermittelt ist (es gibt bisher nur einen Ansatz von Cant et al., 1995), wird an ihrer Stelle häufig auf die strukturelle Komplexität ausgewichen. Die strukturelle Komplexität ist mit der kognitiven Komplexität korreliert und kann objektiv und relativ einfach gemessen werden. Aus diesem Grunde machen viele der taktischen Prinzipien Aussagen zur Struktur.

Vermeide Redundanz

If something is to be done nearly identically at several places in a software product, it should be done in one place that is referenced from the other places.
(Oskarsson, Glass, 1996)

In designing software, I regard the need to code similar functions in two separate programs as an indication of a fundamental error in my thinking.
(Parnas, 1979)

Jede Form von Redundanz stellt ein Risiko dar, da es bei Änderungen leicht zu Inkonsistenzen kommt. Stattdessen sollte eine Funktion auch nur an einer Stelle realisiert werden. Dieses Prinzip (siehe auch Fowler, 2001a) wird selten explizit genannt, wohl weil es als selbstverständlich vorausgesetzt wird. Beck (1996) hat die Entwurfsregel ,say everything once and only once" aufgestellt. Hunt und Thomas (1999) nennen ihre Regel dagegen ,DRY - don't repeat yourself". Dagegen ist es bei Programmierern weit verbreitet, mittels Kopieren und Einfügen Redundanz in den Code einzufügen, weil sich dadurch schneller mehr Zeilen Code erzeugen lassen. Diesem kurzfristigen Vorteil steht der langfristige Nachteil in der Wartung gegenüber, wenn alle diese Kopien konsistent geändert werden müssen. Daher wird Kopieren und Einfügen auch als Anti-Muster aufgefasst (Copy-and-Paste AntiPattern bei Brown et al. (1998). Die genannten Autoren empfehlen alle, Redundanzen durch Refaktorisierung zu entfernen, wenn sie entdeckt wird. Durch entsprechenden Entwurf lässt sich die Entstehung von Redundanz in Code aber bereits im Vorfeld verringern.

Wenn man will, kann man das Modularisierung, Datenabstraktion, Vererbung usw. als Techniken zur Umsetzung dieses Prinzips betrachten (Oskarsson, Glass, 1996).

Taktische Prinzipien

Benutze Abstraktionen

Das Abstrahieren hat die Bedeutung, daß aus dem Konkreten nur zu unserem subjektiven Behuf ein oder das andere Merkmal so herausgenommen werde, daß mit dem Weglassen so vieler anderer Eigenschaften und Beschaffenheiten des Gegenstandes denselben an ihrem Wert und ihrer Würde nichts benommen sein solle.
(G. W. F. Hegel)

In the development of our understanding of complex phenomena, the most powerful tool available to the human intellect is abstraction.
(C. A. R. Hoare)

Abstraktion ist ein für den Menschen natürliches Verfahren, mit komplexen Sachverhalten umzugehen (Dörner, 1979). Der IEEE Standard 610.12-1990 definiert Abstraktion (abstraction) wie folgt:

Definition 2-2 (Abstraction, IEEE 610.12-1990)

A view of an object that focuses on the information relevant to a particular purpose and ignores the remainder of the information.

Bei der Abstraktion werden irrelevante Details ausgeblendet. Um einen Sachverhalt zu verstehen, ist es dann nicht mehr notwendig, alle Details auf einmal im Gedächtnis zu haben. Es genügt, die an der aktuellen Aufgabenstellung beteiligten Abstraktionen zu beherrschen.

Viele der folgenden Prinzipien, z.B. zur Modularisierung und Kapselung, sind besondere Spielarten der Abstraktion. Ein Beispiel für Abstraktion sind abstrakte Datentypen (Linden, 1976; Liskov, 1988)

Teile und herrsche

The most fundamental problem in software development is complexity. There is only one basic way of dealing with complexity: Divide and conquer.
(Bjarne Stroustrup)

Das Prinzip ,Teile und herrsche" hat sich bereits bei der Beherrschung von Weltreichen bewährt. Im Bereich des Entwurfs sieht es folgendermaßen aus: Ein Problem wird in kleinere, möglichst unabhängige Teilprobleme zerlegt, die gelöst werden. Aus den Einzellösungen wird dann die Gesamtlösung zusammengesetzt. Das Verfahren läßt sich rekursiv anwenden, bis man zu einfach lösbaren Teilproblemen gelangt. Eine Anwendung dieses Verfahrens ist z. B. die schrittweise Verfeinerung (stepwise refinement) von Wirth (1971).

Der Vorteil dieses Vorgehens besteht darin, dass zu einem Zeitpunkt nur ein Problem relativ geringer Komplexität gelöst werden muss. Bereits gelöste Probleme können dann als atomare Bausteine (unter Abstraktion ihres inneren Aufbaus) für die Lösung des übergeordneten Problems verwendet werden. Die resultierende Lösungsstruktur lässt sich auch leichter auf verschiedene Rechnerknoten verteilen oder parallelisieren.

Strukturiere hierarchisch

Simon (1962) stellt fest, dass der Mensch nur dann mit komplexen Systemen umgehen kann, wenn sie hierarchisch sind (oder sich hierarchisch beschreiben lassen): ,The fact, then, that many complex systems have a nearly composable, hierarchic structure is a major facilitating factor enabling us to understand, to describe, and even to 'see' such systems".

Ein Grund für die Bevorzugung hierarchischer Strukturen sind die beschränkten Fähigkeiten des menschlichen Kurzzeitgedächtnisses, das für alle Denkvorgänge benötigt wird. Nach Miller (1956) kann der Mensch etwa sieben Informationseinheiten gleichzeitig im Kurzzeitgedächtnis halten; bei schwierigen Aufgaben auch weniger (Kintsch, 1977). Andererseits kann die Kapazität vergrößert werden, wenn die Informationseinheiten in sich geeignet strukturiert sind, so dass Abstraktion von Details möglich wird. Hierarchische Strukturierung scheint hierbei die geeigneteste zu sein.

Parnas (1972c) empfiehlt ebenfalls eine hierarchische Zerlegung eines Programms in Module, wobei er auf das THE-System von Dijkstra (1968) als gutes Beispiel verweist, das aus hierarchischen Schichten aufgebaut ist. Als zusätzlichen Vorteil nennt Parnas die Wiederverwendbarkeit von Teilen des Systems: ,The existence of the hierarchical structure assures us that we can `prune' off the upper levels of the tree and start a new tree on the old trunk."

Parnas (1974) stellt aber auch fest, dass man bei dem Begriff ,hierarchisch" aufpassen muss, da es verschiedene Arten der Hierarchie gibt. Als Beispiele nennt er unter anderem die Benutzt-Hierarchie und die Enthält-Hierarchie von Modulen. Diese Hierarchien sind statisch existent. Parnas nennt aber auch Hierarchien, die erst zur Laufzeit auftreten, wie zum Beispiel die von ihm so genannte ,Haberman-Hierarchie", bei der die Delegation von Aufgaben oder Teilaufgaben nur in eine Richtung passieren darf, so dass Zyklen ausgeschlossen sind. Bei solchen Systemen ist es zum Beispiel es einfacher, die Korrektheit nachzuweisen.

Modularisiere

The only problems we can really solve in a satisfactory manner are those that finally admit a nicely factored solution.
(Dijkstra, 1972)

Der IEEE Standard 610.12-1990 definiert Modularisierung (modularization) wie folgt:

Definition 2-3 (Modularization, IEEE 610.12-1990)

The process of breaking a system into components to facilitate design and development.

Das Gesamtsystem wird - im Geiste des Teile-und-herrsche-Prinzips - in sinnvolle Subsysteme und Module zerlegt. Das Modul dient dabei als Behälter für Funktionen oder Zuständigkeiten des Systems. Der Modulbegriff ist, vor allem historisch bedingt, unterschiedlich belegt. Er reicht im prozeduralen Paradigma von der Fortran-Subroutine bis hin zum Ada-Paket. Bei der objektorientierten Sichtweise entspricht dem Modul die Klasse.

Eine gute Zerlegung des Systems in Module kann die Entwicklung, Änderung und Wartung des Systems sehr erleichtern. Daher gibt es für die Modularisierung viele leitende Prinzipien, beispielsweise die Kapselung, das Geheimnisprinzip, niedrige Kopplung zwischen Modulen und hohen Zusammenhalt innerhalb des Moduls.

Balzert (1985b) fordert die folgenden Eigenschaften von Modulen:

  • Bereitstellung einer funktionalen Abstraktion oder eines abstrakten Datentyps
  • Kontextunabhängigkeit bis auf eine definierte Schnittstelle
  • Spezifizierung durch Schnittstellenbeschreibung
  • Realisierung des Geheimnisprinzips, d. h. Interna werden vor dem Anwender verborgen
  • Realisierung des Lokalitätsprinzips bezogen auf die Schnittstelle, d. h. die für die Anwendung benötigten Informationen sind an einer Stelle zusammengefasst
  • Realisierung des Prinzips der schmalen Datenkopplung, d h. die Schnittstelle ist schmal und es werden möglichst wenig Daten übergeben
  • im qualitativen und quantitativen Umfang handlich und überschaubar.
Trenne die Zuständigkeiten

Mit dem Prinzip der Trennung der Zuständigkeiten, besser bekannt als separation of concerns (der Begriff geht zurück auf Dijkstra, 1976, S. 211ff.), wird nahegelegt, das Systems anhand von Zuständigkeiten (häufig auch als Verantwortlichkeiten bezeichnet) in Komponenten aufzuteilen. Komponenten, die an der gleichen Aufgabe beteiligt sind, werden gruppiert und von denen abgegrenzt, die für andere Aufgaben zuständig sind. Auf diese Weise entstehen Subsysteme. Falls eine Komponente für mehrere Aufgaben zuständig sein muss, sollten die Bestandteile intern nach Zuständigkeiten gruppiert werden.

Das Prinzip ist, wie auch Kopplung und Zusammenhalt, ein wichtiges Kriterium für die Modularisierung. Werden verschiedene Zuständigkeiten voneinander getrennt, können sich diese unabhängig voneinander ändern. Sind mehrere in einer Komponente zusammengefasst, sind bei der Änderung der Realisierung einer Zuständigkeit alle Verwender der Komponente von der Änderung betroffen, auch wenn sie die geänderte Zuständigkeit gar nicht benutzen.

Ein wichtiges Beispiel für die Anwendung dieses Prinzips ist die Trennung von GUI-Komponenten von Applikationskomponenten (Fowler, 2001b).

Trenne Verhalten und Implementierung

Nach diesem Prinzip (siehe Rumbaugh et al., 1993, S. 344f. und Buschmann et al., 1996, S. 401) soll eine Komponente (oder eine Methode) entweder für das Verhalten (policy) oder die Implementierung (implementation) zuständig sein, nicht für beides (Page-Jones, 1988, S. 104, unterscheidet zwischen ,management (calling and deciding)" und ,work (calculating and editing)"). Eine Komponente für das Verhalten trifft Entscheidungen anhand des Kontextes, interpretiert Ergebnisse und koordiniert andere Komponenten. Da sie die Prozesse der realen Welt modelliert, wird sie sich wahrscheinlich häufig ändern.

Eine Komponente für die Implementierung dagegen führt einen Algorithmus auf vorliegenden Daten aus, die benötigten Informationen werden beim Aufruf mitgegeben. Hier wird in der Regel ein Konzept aus der Lösungswelt modelliert, das stabil ist. Da sie zur Ausführung keine weiteren Informationen benötigt, kann sie leicht aus ihrem Kontext herausgelöst und wiederverwendet werden.

Kapsele Zusammengehöriges

Der IEEE Standard 610.12-1990 definiert Kapselung (encapsulation) wie folgt:

Definition 2-4 (Encapsulation, IEEE 610.12-1990)

A software development technique that consists of isolating a system function or a set of data and operations on those data within a module and providing precise specifications for the module.

Das bedeutet, dass zusammengehörige Bestandteile einer Abstraktion zu einem Ganzen zusammengefasst und von anderen abgegrenzt werden. Bei der objektorientierten Sichtweise wird die Kapselung durch die entsprechende Definition von Klassen erreicht. Abbildung 2-1 verdeutlicht diese Vorgehensweise: Teile der Implementierung werden hinter einer Schnittstelle verborgen. Die Schnittstelle dient dem kontrollierten Zugriff auf die verborgenen Eigenschaften.

Abbildung 2-1: Kapselung

 

Bei der Kapselung werden zusammengehörige Teile zu einem größeren Ganzen zusammengefasst. Anhand von Kriterien wie Kopplung und Zusammenhalt kann entschieden werden, was zusammengehört. Zusammengehörige Teile sind stark voneinander abhängig; bei Änderungen sind sie in der Regel alle gleichzeitig betroffen. Falls sie in einem Modul zusammengefasst sind, bleibt die Änderung auf dieses Modul beschränkt. Sind sie es allerdings nicht, kann die Änderung sehr viele Module betreffen, von denen unter Umständen nicht alle sofort identifiziert werden können, was zu unvollständigen und damit fehlerhaften Änderungen führt.

Trenne Schnittstelle und Implementierung

Nach dieses Prinzip soll eine Komponente aus einer Schnittstelle und einer Implementierung bestehen. Die Schnittstelle definiert die Funktionalität und die Verwendung und ist den Verwendern zugänglich. Die Implementierung dagegen umfasst den tatsächlichen Code für die Funktionalität der Komponente und weitere, nur intern benötigte Funktionalität. Die Trennung schützt die Verwender vor Implementierungsdetails. Auf diese Weise kann Implementierung geändert oder ausgetauscht werden, ohne dass der Verwender davon betroffen ist.

Das Prinzip der Trennung zwischen Schnittstelle und Implementierung ist Voraussetzung für das Geheimnisprinzip.

Vermeide Abhängigkeiten von Details

Das Prinzip der Umkehr der Abhängigkeiten (Dependency Inversion Principle, DIP; Martin, 1996c) fordert, dass Details auf Abstraktionen beruhen sollen, aber nicht umgekehrt. Außerdem sollen Verfahrensweisen auf hoher Ebene nicht von Implementierungsdetails sondern von Abstraktionen abhängig sein.

Der Name des Prinzips kommt daher, dass die Abhängigkeiten bei der prozeduralen Programmierung häufig von den Abstraktionen zu den Details gehen. Abhängigkeiten sind dort durch Prozeduraufrufe gekennzeichnet; in den meisten Fällen rufen die Abstraktionen (d.h. die anwendungsnahen Funktionen) Prozeduren niedrigerer Ebenen (Dienstleistungsprozeduren) auf. Änderungen in den Details können so aber weitreichende Auswirkungen auf die Abstraktionen haben.

Das DIP lässt sich in der Objektorientierung so umsetzen, dass die wesentlichen Abstraktionen in Form von Interfaces 1 oder abstrakten Klassen formuliert sind. Implementierungsdetails können auf diese Weise in Klassen ausgelagert werden, welche die so definierte Schnittstelle implementieren. Zum Beispiel sollte ein Client (ein Verwender) nicht den Server (eine verwendete Klasse) direkt ansprechen. Zwischen den beiden wird stattdessen eine abstrakte Schnittstelle etabliert, von der dann sowohl Client als auch Server abhängen. Das hat zum einen den Vorteil, dass der Server ausgetauscht werden kann, ohne dass der Client davon betroffen ist. Zum anderen können sowohl Client als auch Server in anderer Umgebung wiederverwendet werden, sofern die Schnittstelle erhalten bleibt. Allerdings erkauft man sich diese Vorteile durch eine zusätzliche Klasse im Entwurf.

Sorge für schmale Schnittstellen

Das Schnittstellentrennungsprinzip (Interface Segregation Principle, ISP; Martin, 1996d) fordert: ,Clients should not be forced to depend on interfaces that they do not use." Häufig werden einer Basisklasse Methoden beigefügt, die nur von einigen Subklassen benötigt werden. Dies macht die Schnittstelle unnötig umfangreich, da viele Verwender der Klasse die zusätzlichen Methoden gar nicht brauchen. Falls sich die Schnittstelle aber in diesem Bereich verändert, sind sie dennoch von der Änderung betroffen: Sie müssen zumindest neu compiliert werden. Daher ist es sinnvoll, in der Basisklasse nur die für die eigentliche Abstraktion benötigten Methoden zu definieren. Andere Aspekte sollten durch separate Schnittstellen (z. B. durch Interfaces in Java) definiert werden, die bei Bedarf von den Subklassen hinzugeerbt werden können. Alternativ können auch Adapter (Gamma et al., 1995) eingesetzt werden, um einem Objekt eine andere Schnittstelle zu geben.

Sorge für lose Kopplung

...the best programs are designed in terms of loosely coupled functions that each does a simple task.
(Kernighan, Plauger, 1974)

Kopplung (coupling) ist ein Maß für die Stärke der Verbindung (und damit Abhängigkeit) von Programmkomponenten untereinander. Angestrebt wird eine möglichst niedrige Kopplung von Komponenten. Damit steigt die Wahrscheinlichkeit, dass Änderungen sich nur lokal auf eine Komponente auswirken und nicht auf andere Komponenten ,ausstrahlen". Außerdem wirken sich Fehler in anderen Modulen weniger auf das Modul aus. Die Verständlichkeit wird besser, da zum Verständnis weniger andere Module verstanden werden müssen. Schließlich ist auch die Wiederverwendbarkeit besser, weil sich das Modul besser aus seinem Kontext herauslösen lässt.

Ludewig (1998) unterscheidet folgende Stufen der Kopplung:

  1. Einbruch: Modifikation des Codes einer anderen Komponente (vor allem in Assembler möglich)
  2. volle Öffnung: Zugriff auf alle Daten, z. B. Datenhaltung in globalen Variablen
  3. Fremdsteuerung: der Aufrufer teilt über Steuerparameter mit, wie sich der Aufgerufene zu verhalten hat.
  4. selektive Öffnung: bestimmte Daten sind zugänglich
  5. Parameter: Programmteile sind als Prozeduren formuliert, die nur über Parameter miteinander kommunizieren
  6. Funktionen: nur Wertparameter und Funktionsresultate
  7. keine Kopplung: keine Verbindung zwischen unabhängigen Programmteilen

In der Objektorientierung kann die Kopplung in drei verschiedene Arten unterschieden werden (Li, 1992):

  • Kopplung durch Vererbung: eine Unterklasse ist allein durch das Erben von Eigenschaften mit ihrer Oberklasse gekoppelt. Je mehr geerbt wird, desto höher ist die Kopplung.
  • Kopplung durch Methodenaufruf: jeder Methodenaufruf durch eine anderen Klasse und der Aufruf von Methoden einer Klasse erhöht die Kopplung.
  • Kopplung durch Datenabstraktion: die Verwendung anderer Klassen als Typ von Attributen erhöht die Kopplung.
Sorge für starken Zusammenhalt

Zusammenhalt (cohesion) ist ein Maß für die Stärke der Zusammengehörigkeit von Bestandteilen einer Programmkomponente. Angestrebt wird ein möglichst hoher Zusammenhalt der Bestandteile. Damit steigt die Wahrscheinlichkeit, dass bei einer Änderungen in der Regel nur eine Komponente betroffen ist, da alle Aspekte einer Abstraktion an einem Ort zusammengefasst sind. Ludewig (1998) unterscheidet folgende Stufen des Zusammenhalts:

  1. kein Zusammenhalt: rein zufällige Zusammenstellung (z.B. je 100 Zeilen des Programms)
  2. Ähnlichkeit: ähnlicher Zweck, z. B. Fehlerbehandlungsprozedurensammlung
  3. zeitlich: Ausführung zur gleichen Zeit, z. B. Initialisierungsanweisungen
  4. arbeitet mit denselben Daten: z. B. Datumsoperationen eines Kalenderpakets
  5. Kommunikation über Zwischenergebnisse: der eine Teil verwendet, was der andere erzeugt (typisch für funktionalen Entwurf).
  6. einziger Zweck: z.B. Iterator-Operationen einer Liste
  7. einziges Datum: Abstrakter Datentyp oder Datenkapsel

Die Begriffe Kopplung und Zusammenhalt wurden von Stevens et al. (1974) eingeführt und durch ähnliche Skalen wie bei Ludewig (1998) definiert. Die Prinzipien lassen sich auf Alexander (1964) zurückführen, der zwei Entwurfsprinzipien postulierte: die Schnittstelle der Einheiten des Entwurfs nach außen sollten so einfach wie möglich sein, und die Einheiten selbst sollten nur einem einzigen Zweck dienen.

Module sollen offen und geschlossen sein

Meyer (1997, S. 57) formuliert das Offen-Geschlossen-Prinzip (Open-Closed Principle, OCP) wie folgt: ,Modules should be both open and closed." Geschlossen (closed) bedeutet hier: Das Modul kann gefahrlos verwendet werden, da sich seine Schnittstelle nicht mehr ändert; offen (open) dagegen: Das Modul kann problemlos erweitert werden. Diese beiden Forderungen sind im imperativen Programmierparadigma unvereinbar, da jede Erweiterung eines Moduls Auswirkungen auf seine Verwender hat; im harmlosesten Fall müssen sie zumindest neu übersetzt werden.

Im objektorientierten Paradigma kann das Dilemma mit Hilfe der Vererbung gelöst werden: die Schnittstelle des geschlossenen Moduls wird in ein Interface (oder eine abstrakte Klasse) umgewandelt. Alle Verwender beziehen sich nur auf dieses Interface. Je nach Bedarf können nun Implementierungen in Form von Subklassen von A erzeugt werden, wobei die Subklassen auch die Funktionalität von A erweitern können.

Martin (1996a) leitet aus dem OCP die folgenden Heuristiken ab:

  • alle Attribute einer Klasse sollten verborgen werden (z.B. durch private-Deklaration in Java/C++)
  • es soll keine globalen Variablen geben
  • die Verwendung dynamischer Typinformation (z.B. RTTI in C++ oder Reflection in Java) ist gefährlich, da sie dem OCP zuwiderlaufen kann
Sorge bei Redefinition für den Erhalt der Semantik

Liskov (1988) formuliert folgende Forderung an die Semantik von Subtypen, die auch als das Liskovsches Substitutionsprinzip (Liskov Substitution Principle, LSP) bezeichnet wird:

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

Übersetzt in objektorientierte Terminologie bedeutet dies eine Forderung an alle Subklassen S einer Klasse T, für die durch T gegebene Schnittstelle dieselbe Semantik anzubieten. Nur dann kann man bei der Verwendung der Schnittstelle einer Klasse sichergehen, dass sich das Programm gleich verhält, wenn eine Instanz einer Subklasse statt einer Instanz der Klasse selbst verwendet wird.

Das LSP ist besonders wichtig bei der Redefinition von Methoden. Erweiterungen von T durch neue Attribute und Methoden in S sind problemlos möglich, dagegen sind Einschränkungen von T (zum Beispiel das Verbergen einer Methode) in S sehr problematisch, da die von T definierte Schnittstelle nicht mehr vollständig eingehalten wird. Wird das LSP eingehalten, kann ein Verwender einer Schnittstelle sich darauf verlassen, dass der Aufruf einer polymorphen Methode immer den gewünschten Effekt haben wird, egal, welche konkrete Methode schließlich ausgeführt wird. Verstöße gegen das LSP hingegen sind äußerst schwierig zu finden, was sich negativ auf die Wartbarkeit auswirkt. Edwards (1997) unterstreicht die Bedeutung des Erhalts der Semantik der Oberklasse bei der Codevererbung und nennt Vererbung, die das LSP einhält, Repräsentationsvererbung (representation inheritance).

Martin (1996b) stellt fest, dass das LSP eine starke Verbindung zu Design by Contract (Meyer, 1997, S. 331ff.) hat. Dort wird gefordert, dass die Vorbedingung einer redefinierten Methode schwächer, die Nachbedingung stärker sein muss, damit die neue Unterklasse den ,Vertrag" ihrer Oberklasse erfüllt. Der Vertrag einer Methode ist der formal gefasste Bestandteil der Semantik einer Methode, der sich in der Programmiersprache Eiffel (Meyer, 1991) in den sogenannten Zusicherungen (assertions) niederschlägt. Allerdings ist die Einhaltung der obigen Redefinitionsbedingung nur eine notwendige Bedingung für die Einhaltung des LSP, in der Regel aber keine hinreichende, da sich manche Aspekte der Semantik (z. B. Antwortzeiten, Speicherbedarf) schlecht formal fassen und in Zusicherungen ausdrücken lassen.

Minimiere die Anzahl der Objekte, mit denen ein Objekt interagiert

Mit dem Demeter-Gesetz (Law of Demeter, LoD) versuchen Lieberherr et al., (1988, 1989), die Anzahl der Objekte, mit der ein Objekt interagiert, einzuschränken und damit die Kopplung zu reduzieren. Es wird verlangt, dass in den Methoden einer Klassen nur Methoden der Klasse selbst, der Argumentobjekte oder der Attributwerte der Klasse aufrufen werden dürfen. Dahinter steht die Überlegung, dass sich die meisten Abhängigkeiten zwischen Klassen durch Methodenaufrufe manifestieren. Durch das Gesetz werden diese Abhängigkeiten beschränkt. Nach Ansicht der Autoren werden dadurch Wartbarkeit und Verständlichkeit gefördert.

Es ist möglich, durch automatische Programmtransformationen Verstöße gegen das Demeter-Gesetz aufzulösen. Das resultierende Programm hat dann allerdings mehr Methoden, weshalb sich die Wartbarkeit wieder verschlechtern kann. In der Regel ist es eher sinnvoll, bei der Feststellung von Verstößen Umstrukturierungen bei den Zuständigkeiten und Beziehungen einzelner Klassen vorzunehmen.

Literatur

Alexander (1964): Alexander, C.: Notes on the Synthesis of Form. Harvard University Press, Cambridge, MA, 1964.

Balzert (1985a): Balzert, H.: Allgemeine Prinzipien des Software Engineering. Angewandte Informatik, 1/1985, 1-8.

Balzert (1985b): Balzert, H.: Phasenspezifische Prinzipien des Software Engineering. Angewandte Informatik, 3/1985, 101-110.

Bass et al. (1998): Bass, L.; Clements, P.; Kazman, R.: Software Architecture in Practice. Addison-Wesley, Reading, MA, 1998.

Beck (1996): Beck, K.: Make it Run, Make it Right: Design Through Refactoring. Smalltalk Report, 6(4), 1997, 19-24.

Boehm (1974): Boehm, B.: Some Steps Towards Formal and Automated Aids to Software Requirements Analysis and Design. In: Rosenfeld, J. (Hrsg.): Information Processing 74: Proceedings of the IFIP Congress 74. North Holland, Amsterdam, 1974, 192-197.

Brooks (1987): Brooks, F.: No Silver Bullet: Essence and Accidents of Software Engineering. IEEE Computer, 20(4), 1987, 10-19.

Brooks (1995): Brooks, F.: The Mythical Man-Month: 20th Anniversary Edition. Addison-Wesley, Reading, MA, 1995.

Brown et al. (1998): Brown, W.; Malveau, R.; McCormick, H.; Mowbray, T.: AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis. Wiley, Chichester, 1998.

Buschmann et al. (1996): Buschmann, F.; Meunier, R.; Rohnert, H.; Sommerlad, P.; Stal, M.: Pattern-Oriented Software Architecture: A System of Patterns. Wiley, Chichester, 1996.

Cant et al. (1995): Cant, S.; Henderson-Sellers, B.; Jeffrey, D.: A Conceptual Model of Coginiteve Complexity of Elements of the Programming Process. Information and Software Technology, 37(7), 1995, 351-362.

Coad, Yourdon (1991): Coad, P.; Yourdon, E.: Object Oriented Design. Prentice Hall, Englewood Cliffs, NJ, 1991.

Davis (1995): Davis, A.: 201 Principles of Software Engineering. McGraw-Hill, New York, 1995.

Dijsktra (1968): Dijkstra, E.: The Structure of the "THE"-Multiprogramming System. Communications of the ACM, 11(5), 1968, 341-346.

Dijkstra (1972): Dijkstra, E.: The Humble Programmer. Communications of the ACM, 15(10), 1972, 859-866.

Dijkstra (1976): Dijkstra, E.: A Discipline of Programming. Prentice-Hall, Englewood Cliffs, NJ, 1976.

Dörner (1976): Dörner, P.: Problemlösen als Informationsverarbeitung. Kohlhammer, Stuttgart, 1976.

Edwards (1997): Edwards, S.: Representation Inheritance: A Safe Form of "White Box" Code Inheritance. IEEE Transactions on Software Engineering, 23(2), 1997, 83-92.

Fairley (1985): Fairley, R.: Software Engineering Concepts. McGraw-Hill, New York, 1985.

Fenton, Pfleeger (1996): Fenton, N.; Pfleeger, S.: Software Metrics: A Rigorous & Practical Approach (2. Auflage). Thomson Computer Press, London, 1996.

Fowler (2001a): Fowler, M.: Avoiding Repetition. IEEE Software, 18(1), 2001, 97-99.

Fowler (2001b): Fowler, M.: Separating User Interface Code. IEEE Software, 18(2), 2001, 96-97.

Hunt, Thomas (1999): Hunt, A.; Thomas, D.: The Pragmatic Programmer: From Journeyman to Master. Addison-Wesley, Reading, MA, 1999.

Gamma et al. (1995): Gamma, E.; Helm, R.; Johnson, R.; Vlissides, J.: Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, Reading, MA, 1995.

Glass (1992): Glass, R.: Building Quality Software. Prentice Hall, Englewood Cliffs, NJ, 1992.

IEEE Std. 610.12-1990: IEEE: IEEE Standard Glossary of Software Engineering Terminology. IEEE Std. 610.12-1990.

Jackson (1975): Jackson, M.: Principles of Program Design. Academic Press, London, 1975.

Kernighan, Plauger (1974): Kernighan, B.; Plauger, P.: The Elements of Programming Style;. McGraw-Hill, New York, 1974.

Kintsch (1977): Kintsch, W.: Memory and Cognition. Wiley, New York, 1977.

Li (1992): Li, W.: Applying Software Maintenance Metrics in the Object-Oriented Software Development Life Cycle. Ph.D. Dissertation, Virginia Polytechnic Institute and State University, Blacksburg, VA, 1992.

Lieberherr et al. (1988): Lieberherr, K.; Holland, I.; Riel, A.: Object-Oriented Programming: An Objective Sense of Style. Proceedings of OOPSLA'88; ACM SIGPLAN Notices, 23(11), 1988, 323-334.

Lieberherr, Holland (1989): Lieberherr, K.; Holland, I.: Assuring Good Style for Object-Oriented Programs. IEEE Software, 6(5), 1989, 38-48.

Linden (1976): Linden, T.: The Use of Abstract Data Types to Simplify Program Modifications. Proceedings of the Conference on Data: Abstraction, Definition and Structure, Salt Lake City, 1976. ACM SIGPLAN Notices, 8(2), Volume II (1976 Special Issue), 1976, 12-23.

Liskov (1988): Liskov, B.: Data abstraction and hierarchy. ACM SIGPLAN Notices, 23(5), 1988, 17-34.

Ludewig (1998): Ludewig, J.: Software Engineering: Vorläufiges, unvollständiges Skript zur Vorlesung Software Engineering an der Fakultät Informatik der Universität Stuttgart, Dezember 1998.

Martin (1996a): Martin, R.: The Open-Closed Principle. C++ Report, 8(1), 1996. http://www.objectmentor.com/publications/ocp.pdf

Martin (1996b): Martin, R.: The Liskov Substitution Principle. C++ Report, 8(3), 1996. http://www.objectmentor.com/publications/lsp.pdf

Martin (1996c): Martin, R.: The Dependency Inversion Principle. C++ Report, 8(5), 1996. http://www.objectmentor.com/publications/dip.pdf

Martin (1996d): Martin, R.: The Interface Segregation Principle. C++ Report, 8(8), 1996. http://www.objectmentor.com/publications/isp.pdf

Meyer (1997): Meyer, B.: Object-Oriented Software Construction (2. Auflage). Prentice Hall, Upper Saddle River, NJ, 1997.

Miller (1956): Miller, G.: The magical number seven plus or minus two: Some limits to our capacity for processing information. The Psychological Review, 63(2), 81-97.

Oskarsson, Glass (1996): Oskarsson, Ö.; Glass, R.: An ISO 9000 Approach to Building Quality Software. Prentice-Hall, Upper Saddle River, NJ, 1996.

Page-Jones (1988): Page-Jones, M.: The Practical Guide to Structured Systems Design (2. Auflage). Prentice-Hall, Englewood Cliffs, New Jersey, 1988.

Parnas (1972a): Parnas, D.: A Technique for Software Module Specification with Examples. Communications of the ACM, 15(5), 1972, 1053-1058.

Parnas (1972b): Parnas, D.: Information Distribution Aspects of Design Methodology. In: Freiman, C. (Hrsg.): Information Processing 71, Volume I - Foundations and Systems. North Holland Publishing Company, Amsterdam, 1972, 339-344.

Parnas (1972c): Parnas, D.: On the Criteria To Be Used in Decomposing Systems into Modules. Communications of the ACM, 15(12), 1972, 1053-1058.

Parnas (1974): Parnas, D.: On a 'Buzzword': Hierarchical Structure. In: Rosenfeld, J. (Hrsg.): Information Processing 74. North Holland Publishing Company, Amsterdam, 1974, 336-340.

Parnas (1979): Parnas, D.: Designing Software for Ease of Extension and Contraction. IEEE Transactions on Software Engineering, (3), 1979, 128-138.

Ross et al. (1975): Ross, D.; Goodenough, J.; Irvine, C.: Software Engineering: Process, Principles, and Goals. IEEE Computer, 14(5), 1975, 17-27.

Rumbaugh et al. (1993): Rumbaugh, J.; Blaha, M.; Premerlani, W.; Eddy, F.; Lorensen, W.: Objektorientiertes Modellieren und Entwerfen. Hanser, München, 1993.

Simon (1962): Simon, H.: The Architecture of Complexity. Proceedings of the American Philosophical Society, 106(6), 1962, 467-482.

Stevens et al. (1974): Stevens, W.; Myers, G.; Constantine, L.: Structured Design. IBM Systems Journal, 13(2), 1974, 115-139.

Wirth (1971): Wirth, N.: Program Development by Stepwise Refinement. Communications of the ACM, 14(4), 1971, 221-227.

Witt et al. (1994): Witt, B.; Baker, F.; Merritt, E.: Software Architecture and Design: Principles, Models, and Methods. Van Nostrand Reinhold, New York, 1994.


1. Der Begriff Interface wird hier im Sinne der Unified Modeling Language (UML) und der objektorientierten Programmiersprache Java verwendet: eine zustandslose abstrakte Klasse, die nur dazu dient, ein bestimmtes Verhalten (eine Schnittstelle) zu vererben. Der Begriff Schnittstelle steht hier für den nach außen sichtbaren Teil einer Klasse; das Gegenstück zur Schnittstelle ist die Implementierung.