Zum Inhalt springen
L

Wikipedia · einfach zusammengefasst · Stand

Prinzipien objektorientierten Designs

Prinzipien objektorientierten Designs sind Prinzipien, die zu gutem objektorientierten Design führen sollen. Sie wurden neben anderen von Robert C. Martin, …

Inhalt4 Abschnitte
  1. 1. Grundidee und SOLID
  2. 2. Weitere Entwurfsprinzipien
  3. 3. Packages: Wiederverwendung und Änderungen
  4. 4. Stabilität und Abstraktheit von Packages

Grundidee und SOLID

Prinzipien objektorientierten Designs sind Leitlinien für den Entwurf wartbarer objektorientierter Software. Sie sollen die Lebensdauer und Änderbarkeit von Software verbessern. Viele objektorientierte Techniken, darunter Entwurfsmuster, Domain-driven Design und Dependency Injection, beruhen auf solchen Prinzipien. Eine besonders bekannte Gruppe bildet das von Robert C. Martin geprägte Akronym SOLID: Single Responsibility, Open-Closed, Liskovsches Substitutions-, Interface-Segregation- und Dependency-Inversion-Prinzip.

Das Single-Responsibility-Prinzip fordert, dass eine Klasse nur eine einzige Verantwortung besitzt. Verantwortung bedeutet dabei „Grund zur Änderung“: „Es sollte nie mehr als einen Grund dafür geben, eine Klasse zu ändern.“ Mehrere Verantwortungen erhöhen die Zahl möglicher späterer Änderungen und damit das Risiko subtiler Fehler. Das Prinzip führt typischerweise zu hoher Kohäsion, also einem starken gemeinsamen Zusammenhang der Methoden einer Klasse.

Das Open-Closed-Prinzip besagt, dass Softwareeinheiten wie Module, Klassen und Methoden für Erweiterungen offen, für Änderungen ihres bestehenden Verhaltens aber geschlossen sein sollen. Bertrand Meyer formulierte es 1988: „Module sollten sowohl offen (für Erweiterungen), als auch geschlossen (für Modifikationen) sein.“ Vererbung kann beispielsweise eine Klasse um zusätzliche Funktionen oder Daten erweitern, ohne das vorhandene Verhalten der Basisklasse zu verändern. Überschriebene Methoden verändern das Verhalten der abgeleiteten Klasse; bei Beachtung des Liskovschen Substitutionsprinzips ändern sie nicht die Erwartungen an die Basisklasse.

Das Liskovsche Substitutionsprinzip (LSP) beziehungsweise Ersetzbarkeitsprinzip wurde 1993 von Barbara Liskov und Jeannette Wing formuliert. Ein Objekt einer abgeleiteten Klasse muss sich so verhalten, dass es an jeder Stelle, an der ein Objekt der Basisklasse erwartet wird, ohne unerwartete Auswirkungen eingesetzt werden kann. Formal gilt: Ist q(x) eine für Objekte x des Typs T nachweisbare Eigenschaft, dann soll q(y) für Objekte y des Typs S gelten, wenn S ein Subtyp von T ist. Operationen der Superklasse müssen also auch mit Objekten des Subtyps korrekt funktionieren. Objektorientierte Programmiersprachen können Verletzungen dieses Prinzips durch Polymorphie und Vererbung nicht grundsätzlich verhindern; solche Verletzungen sind oft nicht sofort erkennbar.

Das Interface-Segregation-Prinzip verlangt, zu große Interfaces nach den Anforderungen ihrer Clients aufzuteilen. Ein Client soll nur von Interfaces abhängen, die genau die von ihm benötigten Funktionen anbieten: „Clients sollten nicht dazu gezwungen werden, von Interfaces abzuhängen, die sie nicht verwenden.“ Dadurch entstehen entkoppelte und leichter refaktorisierbare Klassen; spätere fachliche oder technische Änderungen erfordern meist nur begrenzte Anpassungen.

Das Dependency-Inversion-Prinzip reduziert die Kopplung zwischen Modulen. Module höherer Ebenen sollen nicht von Modulen niedriger Ebenen abhängen; beide sollen von Abstraktionen abhängen. Abstraktionen sollen nicht von Details abhängen, sondern Details von Abstraktionen. Abhängigkeiten sollen damit von konkreten zu abstrakten Modulen und von abgeleiteten Klassen zu Basisklassen verlaufen. Dadurch werden Modulabhängigkeiten reduziert und insbesondere zyklische Abhängigkeiten vermieden.

Weitere Entwurfsprinzipien

Das Gesetz von Demeter (Law of Demeter, LoD) fordert im Kern, dass Objekte nur mit Objekten ihrer unmittelbaren Umgebung kommunizieren. Bevorzugte Anbieterobjekte einer Methode sind die unmittelbaren Bestandteile des aufrufenden Objekts, die als Argumente übergebenen Objekte, innerhalb der Methode direkt erzeugte Objekte und Objekte in globalen Variablen. Jedes Objekt, an das die Methode eine Nachricht sendet, muss zu dieser Gruppe gehören. Dadurch soll die Kopplung sinken und die Wartbarkeit steigen. Die formale Beschreibung lässt sich als automatisch prüfbare Softwaremetrik zur Früherkennung von Kopplungsproblemen verwenden.

Design by Contract, auch Programming by Contract, beschreibt Schnittstellen durch formale Verträge, die über ihre statische Definition hinausgehen. Ein Vertrag besteht aus Vorbedingungen, die der Aufrufer einhalten muss, Nachbedingungen, die der Aufgerufene garantiert, und Invarianten, die den gültigen „Gesundheitszustand“ einer Klasse festlegen. Eine Vertragsverletzung soll bereits in der Entwicklungsphase sofort zu einem Fehler führen (Fail-Fast). Explizite Verträge dokumentieren außerdem das Verhalten eines Moduls und können seine Erlernbarkeit und Wartbarkeit verbessern.

Datenkapselung, auch Information Hiding, verbirgt Daten und interne Informationen vor dem Zugriff von außen. Der direkte Zugriff auf die interne Datenstruktur wird verhindert; stattdessen erfolgt der Zugriff über definierte Schnittstellen. Zugriffsarten wie private und protected sowie Entwurfsmuster wie Facade unterstützen diese Umsetzung.

Das Linguistic-Modular-Units-Prinzip fordert, dass Module syntaktischen Einheiten der verwendeten Programmier-, Design- oder Spezifikationssprache entsprechen. In einer Programmiersprache müssen Module daher separat voneinander kompilierbare Einheiten dieser Sprache sein.

Nach dem Self-Documentation-Prinzip sollen alle Informationen über ein Modul im Modul selbst enthalten sein. Der Code soll möglichst für sich sprechen; technische Dokumentation soll möglichst nahe am Quellcode liegen, beispielsweise durch Javadoc. So bleibt die Dokumentation eher mit dem Code übereinstimmend, und der Code muss so verständlich und wenig komplex sein, dass keine umfangreiche Zusatzdokumentation nötig ist.

Das Uniform-Access-Prinzip verlangt eine gleichartige Notation für alle Services eines Moduls. Von außen soll nicht erkennbar sein, ob ein Service auf gespeicherte Daten zugreift oder eine Berechnung ausführt. Sichtbar sein soll nur, welche Leistung angeboten wird, nicht wie sie intern erbracht wird.

Das Single-Choice-Prinzip besagt, dass eine Menge von Alternativen, etwa Datenvarianten oder Algorithmen, an genau einer Stelle im System vollständig bekannt sein soll. Das Entwurfsmuster Abstrakte Fabrik kann dies unterstützen: Die Fabrik entscheidet einmal, welche Klassen und Algorithmen verwendet werden; dieselbe Entscheidung muss an keiner anderen Stelle wiederholt werden.

Das Persistence-Closure-Prinzip fordert, dass ein Speichermechanismus ein Objekt zusammen mit seinen Abhängigkeiten speichert. Beim Laden müssen auch bisher noch nicht geladene Abhängigkeiten geladen werden. Dadurch sollen Objekte ihre Eigenschaften durch Speichern und anschließendes Laden nicht verändern.

Das Command-Query-Separation-Prinzip (CQS) trennt Abfragen und Kommandos. Eine Abfrage liefert Daten zurück und darf keine beobachtbaren Nebenwirkungen auf den Systemzustand besitzen. Ein Kommando, auch Modifier oder Mutator genannt, erzeugt beobachtbare Nebenwirkungen und liefert keine Daten zurück.

Das Principle of Least Surprise, das Prinzip der geringsten Überraschung, stammt ursprünglich aus Software-Ergonomie, Mensch-Computer-Interaktion und Interfacedesign. Auf Quellcode angewandt bedeutet es, Variablen, Funktionen, Methoden, Parameter und Klassen so zu benennen, dass ihre Funktion und mögliche Seiteneffekte allein aus dem Namen möglichst klar hervorgehen.

Packages: Wiederverwendung und Änderungen

Die Packaging-Prinzipien regeln, wie Klassen zu Modulen beziehungsweise Packages gruppiert werden. Ihr gemeinsames Ziel sind hohe Kohäsion innerhalb eines Moduls und geringe Kopplung zwischen Modulen.

Das Reuse-Release-Equivalence-Prinzip besagt: „Entweder sind alle oder gar keine Klassen eines Packages wiederverwendbar.“ Ein Package gilt dabei als kleinste Einheit eines Releases. Wenn es wiederverwendbar ist, müssen Änderungen nachvollziehbar und der Versionsverlauf rückverfolgbar sein. Außerdem sollen nur Klassen enthalten sein, die für dieselbe Benutzergruppe gedacht sind.

Das Common-Closure-Prinzip verlangt, dass die Klassen eines Moduls gegenüber derselben Art von Änderungen gemeinsam geschlossen sind. Eine Anforderungsänderung, die eine Klasse des Moduls betrifft, sollte auch die anderen Klassen dieses Moduls betreffen. So können zukünftige Änderungen auf wenige Packages begrenzt werden und verursachen weniger Seiteneffekte.

Das Common-Reuse-Prinzip behandelt die gemeinsame Verwendung von Klassen: Klassen, die gemeinsam wiederverwendet werden, sollen auch gemeinsam in einem Modul liegen. Dadurch werden fachlich oder technisch zusammengehörende Einheiten gebildet. Umgekehrt gilt: Wer eine Klasse eines Packages wiederverwendet, sollte grundsätzlich alle Klassen dieses Packages wiederverwenden.

Das Acyclic-Dependencies-Prinzip fordert eine zyklenfreie Abhängigkeitsstruktur. Packages müssen einem gerichteten azyklischen Graphen (DAG) entsprechen; kein Package darf direkt oder indirekt von einem anderen Package abhängen, das seinerseits wieder vom ersten abhängt.

Zyklen können prinzipiell auf zwei Arten aufgebrochen werden. Erstens kann das Dependency-Inversion-Prinzip angewandt werden: Package A führt Interfaces für die von B benötigten Methoden ein, und Klassen in B implementieren diese Interfaces. Dadurch wird die Abhängigkeit zwischen A und B invertiert. Zweitens können alle Klassen eines Zyklus in ein eigenes Package verschoben und zusätzliche Packages für die außerhalb benötigten Klassen eingeführt werden.

Stabilität und Abstraktheit von Packages

Das Stable-Dependencies-Prinzip fordert, dass Abhängigkeiten in Richtung der größeren Stabilität verlaufen. Ein Package soll nur von Packages abhängen, die stabiler sind als es selbst. Stabilität wird dabei indirekt über Instabilität gemessen. Die Instabilität lautet:

I = Aa / (Ae + Aa)

Dabei ist I die Instabilität eines Moduls, Ae sind eingehende Abhängigkeiten (Klassen außerhalb des Moduls, die von Klassen innerhalb des Moduls abhängen), und Aa sind ausgehende Abhängigkeiten (Klassen des Moduls, die von Klassen außerhalb des Moduls abhängen). I liegt zwischen 0 und 1: 0 steht für ein maximal stabiles, 1 für ein maximal instabiles Modul.

Das Stable-Abstractions-Prinzip verlangt, dass die Abstraktheit eines Moduls proportional zu seiner Stabilität ist. Maximal stabile Packages sollen maximal abstrakt, instabile Packages konkret sein. Abstraktheit bedeutet hier insbesondere den Anteil abstrakter Klassen, Interfaces und abstrakter Methoden. Für ein Modul gilt:

A = Ka / K

A ist die Abstraktheit, Ka die Anzahl abstrakter Klassen und K die Gesamtanzahl der Klassen des Moduls.

Die Entfernung eines Moduls von der idealen Linie (Main Sequence) zwischen maximaler Stabilität und Abstraktheit sowie maximaler Instabilität und Konkretheit wird berechnet als:

D = (A + I − 1) / √2

D ist die Distanz zur idealen Linie, A die Abstraktheit und I die Instabilität. D reicht von 0 bis ungefähr 0,707. Je größer die Distanz ist, desto schlechter ist das Stable-Abstractions-Prinzip erfüllt.

Weiterlesen

Objektorientierte Analyse und Design In der Analyse geht es darum, die Anforderungen zu erfassen und zu beschreiben, die das zu entwickelnde Softwaresystem erfüllen soll. In dieser Phase werden … Objektorientierung Unter Objektorientierung (kurz OO) versteht man in der Softwaretechnik eine Sichtweise auf komplexe Systeme, bei der ein System durch das Zusammenspiel … Akronym Akronyme, die aus Initialen zusammengesetzt sind und ausbuchstabiert werden und mit Endbetonung ausgesprochen werden, wie WM · Akronyme, die aus Initialen … Klasse (Objektorientierung) Die Klasse dient als Bauplan für die Abbildung von realen Objekten in Softwareobjekte und beschreibt Attribute (Eigenschaften) und Methoden (Verhaltensweisen) … Methode (Programmierung) Methoden (englisch method oder member function) sind in der objektorientierten Programmierung Unterprogramme in der Form von Funktionen oder Prozeduren, … Vererbung (Programmierung) Die Vererbung dient dazu, aufbauend auf existierenden Klassen neue zu schaffen, wobei die Beziehung zwischen ursprünglicher und neuer Klasse dauerhaft ist. Eine … Abgeleitete Klasse Die abgeleitete Klasse erbt alle Attribute (Member) und Methoden der Basisklasse, kann dabei aber nur auf die nicht als privat deklarierten direkt zugreifen. Polymorphie (Programmierung) Polymorphie oder Polymorphismus (griechisch für Vielgestaltigkeit) ist ein Konzept in der objektorientierten Programmierung, das ermöglicht, … Kopplung (Softwareentwicklung) Unter Kopplung versteht man in der Informatik die Verknüpfung von verschiedenen Systemen, Anwendungen oder Softwaremodulen sowie ein Maß, das die Stärke … Refactoring Refactoring ist ein zentraler Bestandteil der Agilen Softwareentwicklung. Dort wird meist von „kontinuierlichem“ Refactoring oder „kompromisslosem“ Refactoring … Datenstruktur In der Informatik und Softwaretechnik ist eine Datenstruktur ein Objekt, welches zur Speicherung und Organisation von Daten dient. Es handelt sich um eine … Komplexität (Informatik) Die Komplexität eines Problems ist zum Beispiel entscheidend für die Kryptographie und insbesondere für die asymmetrische Verschlüsselung: So verlässt sich …