2. Dezember 2021

8 DINGE, DIE ICH ÜBER OBJEKTORIENTIERTES DESIGN GELERNT HABE (PART 1)

Von steffenboe
rss

Softwaredesign ist eine Kunst für sich. Es gibt viele Regeln darüber, wie „guter“ Code auszusehen hat, und als Junior-Softwareentwickler fängt man gezwungenermaßen an, sich mehr oder weniger „blind“ an diese Regeln zu halten.

Erst mit zunehmender Erfahrung und/oder einer tieferen Auseinandersetzung mit der Thematik wird klar: das starre Befolgen von Clean Code-Regeln führt nicht zu gutem Softwaredesign. Gerade hinsichtlich objektorientierter Programmierung werden vielmehr dessen Konzepte, Ideen und Prinzipien benötigt, um am Ende gleichermaßen verständliche wie flexible Software zu produzieren.

Programmierung selbst macht im Informatikstudium nur einen sehr kleinen Teil des Curriculums aus. Mit dem Praxiseinstieg können die meisten Studenten daher zwar grundlegende Java-Programmierung, sind aber mit den zugrundeliegenden Konzepten nur wenig vertraut. In der Praxis kommen dann andere Herausforderungen dazu: wenig Zeit, knappes Budget, gestresste Kollegen. Kein ideales Umfeld, um über sich und seine Fähigkeiten hinauszuwachsen. Es müssen neue Features geliefert und Bugs behoben werden, man steht vor schwierigen technischen Problemen.

Und sind diese Probleme einmal gelöst, bleibt wenig Zeit zum Reflektieren, Lernen und Verbessern.

Objektorientiertes Design verspricht viel, aber falsch angewendet kann es wenig davon halten.

Daher folgt hier eine Liste von Konzepten und objektorientierten Prinzipien, die in der Praxis schlichtweg wenig bekannt oder oft vernachlässigt sind.


1. Objekte kapseln Komplexität

Komplexität ist das Kriterium, das über die Kosten eines Softwaresystems entscheidet. John Ousterhout definiert Komplexität in seinem Buch „A Philosophy of Software Design“ wie folgt:

„Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.“

A Philosophy of Software Design, John Ousterhout

Ein komplexes Softwaresystem bedeutet also immer: hoher Änderungsaufwand.

Softwaredesign, und im speziellen objektorientiertes Softwaredesign, dient dem Zweck, diese Komplexität bewusst zu verteilen und damit die Begreifbarkeit und Flexibilität eines Softwaresystems langfristig zu gewährleisten.

Objektorientierte Design erreicht das, indem schwierige, komplexe Aufgaben in einfachere, leichter zu verstehende Aufgaben aufgeteilt werden. Dafür bedient sich die objektorientierte Programmierung Objekten, denn die haben die Aufgabe, Komplexität zu kapseln und vor dem Aufrufer verbergen. Die so gekapselte Funktionalität bieten Objekte über ihr Interface an.

Aber: jedes neue Interface führt einem Softwaresystem Komplexität zu, und sollte daher auch einen angemessenen Mehrwert liefern, um für diese verursachte Komplexität aufzukommen.

Ein fehlendes Softwaredesign ist dann erkennbar, wenn es entweder zu wenig Objekte (die jeweils zu viel Komplexität kapseln), oder zu viele Objekte (die kapseln jeweils zu wenig Komplexität) gibt. Ersteres nennt sich prozedurale Programmierung, letzteres ist unter anderem ein Problem des missverstandenen Single-Responsibility-Prinzips. Womit wir beim nächsten Thema wären.

2. Objekte haben einen Zweck

In der Praxis führt die Anwendung des „Prinzips einer einzigen Verantwortlichkeit“ nämlich häufig dazu, dass zu viele kleinteilige und „flache“ Klassen (Klassen mit wenig Funktionalität) angelegt werden. Solche Klassen haben das Problem, dass sie Komplexität nicht kapseln, sondern auf die Kommunikation zwischen den Objekten verlagern.

Der Name des „Prinzips der einzigen Verantwortlichkeit“ ist irreführend. Was genau bedeutet denn „eine Verantwortlichkeit“? „Verantwortlichkeit“ ist so abstrakt, das könnte alles sein.

Ist das Persistieren einer Datenstruktur in einer Datenbank „eine einzige Verantwortlichkeit“? Oder das Aufbauen und Aufrechterhalten einer Datenbankverbindung? Ist das Schreiben in die Datenbank „eine einzige Verantwortlichkeit“, genau wie das Lesen aus der Datenbank?

Was ich damit zeigen möchte, ist, dass der Begriff der Verantwortlichkeit in hohem Maße von der Abstraktionsebene abhängt, auf der man sich gerade befindet. Jeder Klasse kann genauso leicht eine „einzige“ Verantwortlichkeit zugeschrieben werden, wie mehrere Verantwortlichkeiten, je nachdem, auf welchem Abstraktionsgrad man gerade argumentiert.

Da hilft auch die weit bekannte Definition „ein einziger Grund zur Änderung“ nicht weiter, denn absolut alles kann sich ändern.

Passender ist es, von einem Zweck, einem „Purpose“ im System zu sprechen. Eine Klasse sollte in einem System einen klar definierten, wohlüberlegten Zweck besitzen.

Mit solch einem Zweck werden eventuell mehrere Verantwortlichkeiten einhergehen. Das ist nicht schlimm, wenn diese Verantwortlichkeiten untrennbar und kohärent mit dem Zweck der Klasse verbunden sind.

Die Designfrage ist daher eher, welche Verantwortlichkeiten gehören zu einem Zweck, sollten also gemeinsam einem Objekt zugeordnet werden?

Um wieder auf die Komplexitätsthematik zurückzukommen, was ist weniger komplex: 10.000 Klassen mit jeweils „einer einzigen Verantwortlichkeit“, dann aber auch mit jeweils weniger Funktionalität und entsprechend höheren Kommunikationsbedarf. Oder 1.000 Klassen mit jeweils einem klar definierten Zweck in einem System, jeweils mehr Funktionalität, eine geordnete Kommunikation?

Das „Prinzip einer einzigen Verantwortlichkeit“ ist kein Ersatz für ein objektorientiertes Design.

Auch Domänenobjekte (aus dem domänengetriebenen Design) spiegeln tatsächliche Objekte aus der Problemdomäne wider, mit all ihren Attributen und Verhaltensweisen. Und diese sind kaum mit Hinblick auf das Prinzip einer einzigen Verantwortlichkeit gestaltet, sondern eher das Ergebnis jahrelanger Auseinandersetzung der Fachexperten mit der Problemdomäne.

3. Objekte sind selbstständig

Das bedeutet, sie müssen nicht von anderen Objekten gesteuert werden.

Zentral verwaltete Kontrollflüsse tun jedoch genau das. Sie sind ein Beispiel dafür, wie schnell man auch in objektorientierten Sprachen in die prozedurale Programmierung abrutschen kann.

Sie sind erstens problematisch, weil sie schlecht skalieren: je größer die Anwendung wird, desto mehr Funktionalität müssen zentrale Controller verwalten. Damit ziehen sie mehr und mehr Abhängigkeiten an und müssen bei jeder Modifikation des Systems wahrscheinlich ebenfalls mit angepasst werden müssen. Das verursacht zusätzlichen Änderungsaufwand.

Zweitens besitzen Controller-Klassen ein weiteres, fast schon problematischeres Merkmal: sie wissen, wie andere Objekte zusammenspielen müssen, um ein gewisses Problem zu lösen. Controller müssen die Objekte im Detail kennen, die sie orchestrieren. Damit läuft man Gefahr, dass Objekte so speziell modelliert werden, dass sie nur in einem ganz bestimmten Kontext funktionieren.

Objekte sind selbstständig und sie befehligen keine anderen Objekte. Objekte entscheiden selbst, wie und mit wem sie kooperieren, um eine Aufgabe zu bewältigen. Das ist ein zentrales Prinzip, mit dem die Objektorientierung Flexibilität erreicht.

Das steht im Widerspruch zur oben angesprochenen Komplexität, denn der Kontrollfluss ist, wenn er entsprechend verteilt wird, erstmal schwerer nachzuvollziehen. Das ist aber ein Trade-Off, den die Objektorientierung zugunsten der Flexibilität eingeht.

4. Objekte sind höflich zueinander

Objekte sollten andere Objekte bitten, etwas für sie zu tun, anstatt ihnen vorzugeben, wie etwas zu tun ist. Der Unterschied mag subtil erscheinen, hat aber weitreichende Auswirkungen: diktiert ein Objekt einem anderen, wie es etwas zu erledigen hat, ist letzteres Objekt nicht mehr selbstständig, und es kommt zu einem Informationsleck.

Das Wissen, wie etwas getan werden muss, sollte von einem Objekt sorgfältig gekapselt werden, anstatt durch Nachrichten zwischen den Objekten hin und her zu wandern.

Das bringt Flexibilität für kommende Änderungen: ist bestimmtes Wissen in einem Objekt gekapselt, muss nur dieses eine Objekt angepasst werden. Tritt dieses Wissen jedoch durch die Kommunikation von Objekten nach außen, müssen immer mindestens zwei Objekte auf entsprechende Änderungen reagieren.

Bietet ein Objekt viele Methoden über sein öffentliches Interface an, die von anderen Objekten erst noch „zusammengefügt“ werden müssen, ist das ein Zeichen dafür, dass ein Objekt einem anderen vorgibt, wie etwas getan werden soll, anstatt um etwas zu bitten.


4 von 8 geschaffte, hurra! Im nächsten Beitrag schauen wir uns die restlichen 4 Konzepte/Ideen an. Die Beiträge sind bewusst generisch und abstrakt gehalten: zukünftige Beiträge zeigen dann konkrete Beispiele.

rss