Über unsMediaKontaktImpressum
Andre Krämer 04. September 2018

SOLID [5] – Das Open Closed Principle

Dies ist der letzte Teil der Artikelserie über die SOLID-Prinzipien des objektorientierten Softwaredesigns. Wir beschäftigen uns darin mit dem Open Closed Principle. Dieses unterscheidet sich von den anderen Prinzipien dadurch, dass es unterschiedliche charakteristische Lesarten dazu gibt. Wir sehen uns diese an und beantworten die Frage, wie es zu diesen Ansichten kam und bewerten ihre Nützlichkeit.

Viele Dinge sind mehr als die Summe ihrer Teile. So soll es auch mit dieser Artikelserie sein. Deshalb blicken wir am Schluss zurück. Nun, wo wir alle SOLID-Prinzipien kennen, betrachten wir sie noch einmal als Ganzes. Wir versuchen eine Essenz aus den Prinzipien zu gewinnen, indem wir die folgenden Fragen beantworten: "Wie spielen die verschiedenen Prinzipien zusammen?", "Sind alle Prinzipien gleich gestaltet und gleich wichtig?", "Wie starte ich am besten, wenn ich die Prinzipien effektiv einsetzen möchte?".

Haben Sie die Einführung in die SOLID-Prinzipien gelesen?

SOLID – Die 5 Prinzipien für objektorientiertes Softwaredesign

SOLID, das sind fünf Prinzipien zum Entwurf und der Entwicklung wartbarer Softwaresysteme. Lernen Sie, Clean Code Development anhand von Praxis-Beispielen kennen.
>> Weiterlesen

Der Werdegang des OCP

Die konkrete Formulierung des Prinzips ist etwas vage:

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

Wir sollen offen sein für Erweiterungen, gleichzeitig aber geschlossen gegenüber Veränderungen? Das klingt schön. Gerne würden wir einem System ein Feature hinzufügen, ohne dass der bestehende Code geändert werden muss. Aber es klingt wie Zauberei. Wie sollen wir denn ein Artefakt um neue Funktionalitäten erweitern – ohne dass es selbst verändert wird?

Darauf gibt es zwei Antworten: Das Open-Closed Principle kommt nämlich nicht ursprünglich von Robert C. Martin, sondern von Bertrand Meyer. Es wird zum ersten Mal beschrieben in Meyers Buch "Object-Oriented Software Construction" von 1988. Meyer und Martin beschreiben unter dem jeweils gleichen Prinzip dabei zwei unterschiedliche Umsetzungen. Die Implementierung des Prinzips von Meyer war aber in den 90er Jahren bereits wieder veraltet. Als Martin die SOLID-Prinzipien aufstellte, behielt er das OCP als Prinzip bei. Er übernahm die abstrakte Formulierung, setzte es aber technisch anders um. Im Endeffekt gibt es dadurch zwei unterschiedliche Ausgestaltungen der selben Idee. Das führt immer wieder zu Missverständnissen und macht die Diskussion über das Prinzip nicht einfacher. Um Klarheit zu schaffen, werden wir beide Umsetzungen diskutieren. Die Doppeldeutung ist aber nur das eine Problem. Das viel grundlegendere ist: Das Prinzip ist in sich meistens nicht erreichbar, kontraproduktiv und widerspricht anderen Prinzipien. Trotzdem ist die Auseinandersetzung damit sehr lehrreich – Sie sollten wissen, warum das OCP ein Irrweg ist.

Klassenvererbung als das neue Wunderwerkzeug

Gehen wir noch einmal an den Anfang. Wie sollen wir denn nun ein System erweitern können, ohne es zu verändern? In der konkreten Ausgestaltung von Meyer war die Antwort: Klassenvererbung. Die damals noch junge Disziplin der Objektorientierung versprach große Verbesserungen bei Wiederverwendbarkeit und Wartbarkeit dadurch, dass konkrete Klassen als Basisklassen für neue Klassen verwendet werden können. Ein konkretes Beispiel: Nehmen wir die .Net-Klasse List<T>. Sie stellt eine Ansammlung von generischen Elementen mit wahlfreiem Zugriff (random access) dar. Die Signatur von List umfasst nahezu 60 Methoden und Properties. Darunter so spezialisierte Funktionalitäten wie Suchen und Sortieren (deren direkte Nutzung durch Linq allerdings obsolet geworden ist), sie stellt also bereits einen wichtigen Baustein in der Softwareentwicklung unter .Net dar. Nun könnten wir uns aber durchaus Dinge vorstellen, die diese Klasse trotzdem noch nicht kann. Sie bietet zum Beispiel nicht die Möglichkeit, sich per Event informieren zu lassen, wenn neue Elemente hinzugefügt, oder vorhandene entfernt werden. Indem wir eine neue Klasse definieren, die von List erbt, können wir diese Funktionalität hinzufügen:

    class ObservableList<T> : List<T>
    {
         public event Action CollectionChanged;
    }

Wir können so von der vorhandenen Funktionalität von List profitieren und müssen nur noch das zusätzliche Event implementieren. Wir müssen nicht komplett "von Null" anfangen.

Besonders elegant ist dabei, dass wir die ObservableList überall einsetzen können, wo bereits List erwartet wird. Durch die Vererbung sind die Klassen kompatibel. Die Teile des Systems, die die neue Funktionalität benötigen, können auf Basis von ObservableList arbeiten. Wenn diese aber, z. B. als Parameter, weitergegeben wird an eine Methode, die nur List kennt, funktioniert die Übergabe trotzdem. Man könnte sagen, dass die Klassenvererbung ein valides Vorgehen ist, das ursprünglich festgelegte Ziel des OCP zu erreichen. Wir haben weder List noch seine ursprünglichen Verwender geändert (sie bleiben "closed"), dennoch haben wir dem System eine neue Funktionalität hinzugefügt und nur die Nutzer des neuen Features angepasst ("open").

Die Probleme der Klassenvererbung

Man kann also nachvollziehen, dass auf Klassenvererbung große Hoffnungen gesetzt wurde. Mehr noch, ganze Vererbungshierarchien sollten entstehen, bei denen relativ allgemeine Basisklassen an der Wurzel stehen, die durch viele neue Klassen angereichert werden können, die die vorhandenen um neue Funktionalitäten erweitern – zur Not auch durch Mehrfachvererbung. Spätestens bei diesem Wort sollten Sie allerdings hellhörig werden. Die Mehrfachvererbung stellte sich nämlich als problematisch heraus und wurde deswegen bei C# und Java weggelassen. Und das ist nur die Spitze des Eisbergs. Je mehr wir uns mit der Klassenvererbung beschäftigen, desto mehr Probleme fallen uns auf.

Die oben gezeigte Klassendefinition von ObservableList lässt sich zwar wie gesehen hinschreiben – ein Erben von List ist nicht verboten (sealed in C#, final in Java). Es ist aber unmöglich, die Funktionalität von ObservableList zu implementieren, denn uns fehlen die entsprechenden Anknüpfungspunkte. Zumindest müssten wir in der Lage sein, die Methoden zum Hinzufügen und Entfernen von Elementen zu überschreiben (override), um unsere Funktionalität umsetzen zu können. Dazu müssten diese Methoden aber als virtual deklariert sein. Weiterhin benötigen wir mit hoher Wahrscheinlichkeit Zugriff auf die Datenfelder der Klasse. Diese müssten dazu als protected deklariert sein, statt als private. Nun ist es aber so, dass der Quellcode von List kein einziges mal die Wörter virtual oder protected beinhaltet.

Man könnte sagen, die Klasse List weigert sich, an der Klassenvererbung teilzunehmen. Doch warum ist das so? Sollte Klassenvererbung als wichtiges Werkzeug nicht auch von zentralen Klassen der .Net-Library unterstützt werden? Die Entwickler der Klasse List haben sich dagegen entschieden. Dies entspricht auch der aktuellen Wahrnehmung von Klassenvererbung als ein Werkzeug, bei dem die Nachteile die Vorteile überwiegen.

Der Entwickler einer Klasse muss zwei Themen voneinander abgrenzen: Den internen Zustand der Klasse, also die privaten Datenfelder, einerseits und die öffentlichen Methoden, die diese Felder verändern, andererseits. Es muss sichergestellt werden, dass die öffentlichen Methoden immer zu sinnvollen Änderungen am internen Zustand führen, egal in welcher Reihenfolge sie aufgerufen werden. Wenn wir eine Klasse sinnvoll als Basisklasse anbieten möchten, verkomplizieren wir diese Situation, indem wir nun nicht nur eine öffentliche API über die public-Methoden anbieten, sondern zusätzlich auch eine interne API über protected-Methoden. Eventuell ist es sogar notwendig, die Datenfelder von private auf protected zu ändern. In diesem Fall könnte eine erbende Klasse diese unkontrolliert ändern. Dadurch können wir als Entwickler der Basisklasse nicht garantieren, dass die abgeleitete Klasse korrekt funktioniert. Nun könnte man sagen, dass in diesem Fall der Entwickler der abgeleiteten Klasse einen Fehler gemacht hat. Doch das möchten wir als Anbieter der Basisklasse trotzdem verhindern. Also müssen wir eine gut verständliche protected-API anbieten und diese dokumentieren. Im Zweifelsfall ist es sogar notwendig, den Quellcode der Basisklasse anzubieten. Auf jeden Fall entsteht für den Entwickler der Basisklasse ein großer Mehraufwand bei der initialen Entwicklung. Bei der Weiterentwicklung spielt es keine Rolle, denn diese kann es de facto nicht mehr geben. Die Verzahnung von Basisklassen mit ihren abgeleiteten Klassen ist in nicht-trivialen Fällen so eng, dass Basisklassen nicht mit vertretbarem Aufwand verändert werden können, ohne dass die abgeleiteten Klassen angepasst werden. Eine solche Versionsionskompatibilität wäre bei einer System-Basisklasse aber nicht vertretbar – ironisch, wenn man bedenkt, dass gerade diese unterschiedliche Änderungsnotwendigkeit von Anbieter und Nutzer das zentrale Versprechen des OCP ist, welches durch Klassenvererbung offensichtlich nicht realistisch erreichbar ist.

Es bliebe noch zu erwähnen, dass wir bisher nur die Perspektive des Anbieters einer Basisklasse betrachtet haben. Für den Entwickler, der erben möchte, ist die Situation aber ebenfalls selten angenehm. Er muss darauf hoffen, dass die protected-API ebenso gut dokumentiert ist, wie die public-API. Und selbst wenn sie es ist, ist es häufig schwer, die Abläufe in der Basisklasse nachzuvollziehen, an die man sich so eng koppelt. Der Zugriff auf den Quellcode ist dann das letzte Hilfsmittel. Wenn man an diesem Punkt angekommen ist, stellt sich aber wirklich die Frage, ob man nicht einfach eine neue Klasse implementiert.

All diese Probleme haben die Wahrnehmung von Klassenvererbung geprägt. Sie wird inzwischen nur noch in den seltensten Fällen als sinnvolles Werkzeug angesehen. Am ehesten noch dort, wo ein einzelner Entwickler eine Familie von Klassen entwickelt und damit Code-Wiederverwendung erreichen möchte. In diesem Fall ist nämlich die Notwendigkeit von Dokumentation und Versionsstabilität durch die Personalunion weniger akut.

Von der konkreten zur abstrakten Klasse

Als Robert C. Martin Mitte der Neunziger die SOLID-Prinzipien popularisierte, waren ihm die Probleme der Klassenvererbung bekannt. Deshalb ersetzte er das inzwischen aus der Mode gekommene Konzept der Vererbung von konkreten Klassen durch dessen modernere Variante: Dem Erben von abstrakten Klassen. Diese lösten die genannten Probleme der bisherigen Klassenvererbung. Abstrakte Klassen bieten keinerlei Code-Wiederverwendung an, da sie nur aus einer API-Definition bestehen. Da sie also keine Implementierung mehr haben, entfallen auch die genannten Probleme, die daraus resultieren, dass die Abläufe der Basisklasse mit der der ableitenden Klasse harmonieren müssen. Stattdessen muss der Entwickler der erbenden Klasse die komplette Implementierung leisten. Dies ist aber weniger ein Problem, da sich inzwischen die Erkenntnis durchgesetzt hatte, dass vorhandene, konkrete Klassen als Teil der Implementierung genutzt werden können. Sowohl für den Bereitsteller einer Klasse, als auch den Nutzer in einer neuen Klasse, ist dies ein insgesamt einfacherer Weg als die komplizierte Zusammenarbeit über die Klassenvererbung.

Um Austauschbarkeit mit anderen Implementierungen zu haben, verbleibt das Erben von einer abstrakten Basisklasse. Dies hat jetzt aber nur noch den Zweck, die Kompatibilität der neuen Klasse mit einem vorhandenen Umfeld zu gewährleisten. Wir haben also das Thema der Code-Wiederverwendung getrennt vom Thema der Typkompatibilität.

Mit diesen Erkenntnissen sieht unser vorheriges Beispiel so aus:

    class ObservableList<T> : AbstractList<T>
    {
        private readonly List<T> _innerList = new List<T>();

        public event Action<T, string> CollectionChanged = (item, operation) => { };

        public void Add(T item)
        {
            _innerList.Add(item);
            CollectionChanged(item, "Add");
        }

        public void Remove(T item)
        {
            _innerList.Remove(item);
            CollectionChanged(item, "Remove");
        }
    }

Die diskutierten Punkte sind hier gut zu erkennen:

  1. Wir erben von AbstractList (Diese Klasse ist nicht Teil von .Net – sie dient nur zur Illustration eines Konzepts). Wenn diese Basisklasse von allen relevanten Collections, hier z. B. List, implementiert wird, ist unsere Implementierung austauschbar zu allen Systemteilen, die sich auf diese API beziehen. Nur wer explizit auch die neuen Funktionalitäten nutzen will, muss ObservableList überhaupt kennen.
  2. Da wir nun von einer abstrakten Klasse erben, müssen wir all ihre Methoden selbst implementieren. Das heißt aber nicht, dass wir nicht eine Abkürzung gehen können. Wir speichern einfach intern eine Liste, zu der wir alle Aufrufe delegieren. Nur dort, wo wir eigenes, neues Verhalten implementieren wollen, schreiben wir es explizit dazu. Dadurch erreichen wir – bis auf ein wenig Boilerplate-Code – das selbe Ziel wie bei der Klassenvererbung: Die Wiederverwendung der Implementierungsleistung von List.

OCP – nun ein sinnvolles Prinzip?

Im Grunde könnten wir nun zufrieden sein und sagen: Durch diese neue Umsetzung des OCP haben wir ein sinnvolles Prinzip gewonnen. Doch ich bin skeptisch. Die zentrale Aussage könnte man nun zusammenfassen mit: "Durch den Einsatz von abstrakten Klassen können wir die Nutzer von Funktionalitäten, von Änderungen oder dem Austausch von Implementierungsklassen abschirmen". Das ist mir aber zu wenig. Dieser Einsatzzweck von abstrakten Klassen ergibt sich direkt aus deren Semantik in den gängigen Programmiersprachen. Für ein Prinzip im Sinne von SOLID ist das zu wenig. Schlimmer noch – was den sinnvollen Einsatz von abstrakten Klassen im objektorientierten Design angeht, gibt es ein viel bedeutenderes Prinzip: Das Dependency Inversion Principle [1]. Wenn es also nur darum ginge, wichtige Aussagen über abstrakte Klassen zu machen, ist das DIP viel lehrreicher als das OCP. Auf diesen Punkt werden wir in der Rückschau der Serie noch näher eingehen. An dieser Stelle möchte ich stattdessen auf ein letztes, großes Problem des OCP eingehen, das es für mich untragbar macht.

Alles bleibt so wie es ist – und sei es nur zum Schein

Die Kernidee des OCP ist das Ziel, neue Features zu einem System hinzuzufügen, ohne es zu verändern. Dieses Ziel scheint zunächst auch sehr attraktiv. In manchen Systemen können neue Funktionen nur hinzugefügt werden, indem an vielen Stellen auf einmal Anpassungen gemacht werden. Und diese Änderungen führen auch oft noch zu völlig unerwarteten Fehlern. Nun zu postulieren, dass es möglich sein muss, einem System Funktionen hinzuzufügen, ohne dass irgendwelche Änderungen am bestehenden Code gemacht werden müssen, ist eine vollkommen übertriebene Gegenreaktion. Abgesehen von diesem Überschwang ist es in der Regel auch schlicht unmöglich. Es ist daher ein falsches Ideal und das verfolgen unmöglicher Ziele ist das Letzte, was man in komplexen Vorhaben wie der Softwareentwicklung gebrauchen kann.

Etwas konkreter gesprochen, versucht das OCP sein Ziel des "nicht Änderns" mittels abstrakter Basisklassen zu erreichen. Alle Nutzer sind nur von der abstrakten Basisklasse abhängig. Dadurch können wir die konkrete Implementierung verbessern oder sogar austauschen, ohne dass diese angepasst werden müssen. Das ideale Beispiel für ein Gelingen dieses Vorhabens ist das Ersetzen von realen Implementierungsklassen durch Test-Doubles (Mocks und Stubs) beim Unit-Testing. Dieser Einsatzzweck ist aber untypisch. Selbst in unserem einfachen Beispiel mit der ObservableList sagten wir, dass durch die Kompatibilität mit AbstractList die Nutzer nicht angepasst werden müssen. Alle Nutzer? Nein. Natürlich muss es einige Nutzer geben, die den konkreten Typ der neuen Implementierungsklasse kennen, oder zumindest ein neues, spezifischeres Interface. In unserem Beispiel hieße das: wenn niemand die neu implementierte ObservableList kennt, ist ihre Fähigkeit der Eventbenachrichtigung unnütz. In der Regel müssen also doch Anpassungen an einigen Nutzern gemacht werden. Der Absolutheitsanspruch des OCP ist also grundsätzlich falsch.

Der Anspruch, der durch das OCP geweckt wird, ist aber in den Köpfen vieler Entwickler verankert. Wie jeder von uns, haben sie schlechte Erfahrungen bei dem Versuch gemacht, Änderungen an komplexen Systemen vorzunehmen. Dies führt oft zu einer Vermeidungshaltung. Notwendige Änderungen an der Implementierung werden zwar gemacht, Schnittstellen aber nicht angepasst. Das führt dazu, dass immer mehr Dinge unkontrolliert "unter der Haube" passieren. Methodenaufrufe erzeugen immer mehr unerwartete Seiteneffekte. Ein wünschenswertes, bewusstes Steuern dieser Effekte durch den Aufrufer würde ja bedeuten, dass man die Schnittstelle anpassen muss. Fehler werden verschluckt, da dies viel einfacher ist, als alle Aufrufer mit einer korrekten Behandlung eines neu entdeckten Fehlers zu versehen. Dies ist aber in vielen Fällen notwendig, um einen korrekten Gesamtablauf sicherzustellen. Und wenn ein neues Design erdacht wird, versteigen sich manche Designer in "zukunftssicheren" und "generischen" Schnittstellen, die so allgemein sind, dass man sie "nie wieder ändern muss" – die allerdings auch keinerlei belastbare Semantik mehr haben. Diese Liste ließe sich fortsetzen.

Das Kernproblem sollte klar geworden sein: Schlecht wartbare Systeme führen zu Änderungs- und Fehlerkaskaden. Wenn man aber Änderungen an Schnittstellen als Ursache dieser Probleme definiert, beschuldigt man den Boten. Sie sind nicht die Ursache des Problems, sondern lediglich der Auslöser. Wenn eine Änderung an einer Komponente Änderungen an ihrer Schnittstelle nach sich zieht, weil sich die Semantik ihres Verhaltens signifikant ändern musste, dann müssen eben auch alle Aufrufer überprüft und eventuell angepasst werden. Wenn dies aus Angst unterbleibt, erweist man Verständlichkeit und Wartbarkeit einen Bärendienst. Man arbeitet dann darauf hin, eben die schlechte Wartbarkeit zu etablieren, die man so fürchtet.

Rückblick und Ausblick

Nun kennen wir also alle SOLID-Prinzipien. Eine wichtige Erkenntnis haben wir dabei gewonnen: Das OCP ist sehr problematisch. Unveränderliche Softwareartefakte sollten wir uns nicht zum Ziel setzen, sondern sie eher misstrauisch betrachten. Das Umfeld unseres Systems ändert sich permanent, dadurch auch seine Requirements. Wenn sich dies nicht im Design des Systems niederschlägt, ist das kein gutes Zeichen.
Zur Klarstellung: Alle Ausführungen was die Ablehnung von fixen Schnittstellen in sich ändernden Systemen betrifft, beziehen sich auf das Thema der objektorientierten Modellierung. Versionierte Außenschnittstellen eines Gesamtsystems, wie sie bei (SOAP-)Webservices typisch sind, oder evolutionäre Schnittstellen im REST-Umfeld müssen anders betrachtet werden. In diesem Fall befinden wir uns aber auf einer ganz anderen Ebene der Softwareentwicklung – Makro vs. Mikro.

Es verbleiben für unsere tägliche Arbeit also vier Prinzipien. Doch was nun? Müssen wir bei der Softwareentwicklung immer alle Prinzipien präsent haben? Sind sie alle gleich wichtig? Und wie spielen sie zusammen? Diese Fragen lassen sich zusammenfassend beantworten: Das Single Responsibility Principle steht für sich alleine und ist extrem relevant für alle Fragen der Softwareentwicklung und darüber hinaus. Die verbleibenden drei Prinzipien – das Dependency Inversion Principle, das Interface Segregation Principle und das Liskov Substituition Principle – sind ebenfalls sehr wichtig. Sie sind aber kaum voneinander trennbar und spielen immer zusammen. Wir haben es also im Alltag mit zwei Konglomeraten zu tun. Zwei Stichwörter helfen uns dabei, im Entwickleralltag zu prüfen, ob eines von beiden für die aktuelle Situation relevant ist. Das SRP leitet uns bei der Aufteilung von Systemen in Einzelteile an. Das Dreigestirn DIP, ISP, LSP beschreibt, wann und wie wir die Kommunikation zwischen den durch das SRP gewonnen Fragmenten gestalten. Man kann sie auch als die Interaktionsprinzipien bezeichnen.

Wir befassen uns nun noch einmal mit dem Zusammenspiel der Interaktionsprinzipien. Dem SRP widmen wir den Abschluss des Artikels.

Die Interaktionsprinzipien

Ein wartbares System besteht aus einem losen Netzwerk an fokussierten Klassen. Es gibt jedoch einen limitierenden Faktor – die Abhängigkeiten zwischen diesen Klassen. Im Idealfall lassen sich mit dem SRP Klassen definieren, die natürliche Grenzen in der Gesamtfunktionalität bilden. Das heißt, die Klassen haben prinzipbedingt nicht viel miteinander zu tun, da sie voneinander unabhängige Teilprobleme modellieren. In der Praxis kommt es aber immer wieder zu einem Problem: Durch die Aufteilung entstehen zwar viele kleine Klassen, diese sind aber so fest miteinander verwoben, dass Verständlichkeit und Wartbarkeit nicht verbessert wurden. Teilweise lässt sich dieser Effekt auf Unerfahrenheit zurückführen. Gerade, wenn man es gewohnt ist, große Klassen zu schreiben, fällt es einem zunächst schwer, die richtigen Schnittlinien zu finden. Es ist aber in der Regel möglich, durch eine andere Aufteilung des Systems die Abhängigkeiten zu reduzieren. Trotzdem hat dieses Vorgehen Grenzen. Ein Aufteilen der Klassen alleine reicht nicht.

An diesem Punkt setzt das Dependency Inversion Principle an. Es ermöglicht uns, das Ziel des SRP weiter zu verfolgen – kleine fokussierte Klassen – und gleichzeitig Abhängigkeiten zu minimieren. Dazu führt das DIP Abstraktionen zwischen den Klassen ein. Das Besondere dabei ist, dass es hier, im Gegensatz zur bisherigen Diskussion, eben gerade nicht um "generische" Abstraktionen geht. Es sind vielmehr Kontrakte, die spezifisch sind für die konkrete Interaktion zwischen einem Nutzer und einem Anbieter einer Funktionalität. Durch diese Fokussierung auf das, was für die beidseitige Interaktion auch wirklich gebraucht wird – nicht mehr und nicht weniger – wird die Kopplung stark reduziert und in einem eigenen Artefakt festgehalten. So wie eine Brandschutztür die Ausweitung von Feuer verhindert, verhindern diese Basisklassen die Ausbreitung von Komplexität.

Das DIP setzt dazu bei seiner Definition auf abstrakte Basisklassen. Das Interface Segregation Principle verfeinert das DIP indem es dazu auffordert, anstatt abstrakter Klassen auf Schnittstellen (Interfaces) zu setzen. Dies ermöglicht es, noch feingranularer zu spezifizieren, als es mit abstrakten Klassen möglich ist. Dadurch lassen sich leichter Familien von Abstraktionen bilden und Probleme, wie die Optionalität von Features ausdrücken.

Zum Schluss kommt das Liskov Substitution Principle. Das LSP ist ein genereller Prüfstein, der immer wieder eingesetzt werden muss, um zu beurteilen, ob eine abstrakte Basisklasse, ein Interface oder deren Implementierung zueinander widerspruchsfrei sind.

Das Single Responsibility Principle

Wenn Sie nur ein einziges Prinzip aus dieser Reihe verinnerlichen können, so sollte es dieses sein. Die technischen Gründe dafür will ich hier nicht noch einmal aufrollen, das ist im dazugehörigen Artikel bereits zur Genüge getan [2]. Nur so viel zur Wiederholung:

Streben sie an, dass sie viele einfache Klassen haben, die jeweils fokussiert sind auf (möglichst) eine Aufgabe – anstatt wenigen Klassen, die komplex und schwer zu durchschauen sind.

Dieses Prinzip ist in seiner engen, objektorientierten Sicht bereits sehr relevant. Es kann, und sollte, jedoch viel allgemeiner betrachtet werden. Nicht nur Klassen profitieren davon, wenn sie klein und fokussiert sind. Alle Artefakte der Softwareentwicklung gewinnen von dieser Struktur. Egal ob Buildskript oder Anforderungsspezifikation – ein Dokument, dass aus vielen kleinen, für sich verständlichen Teilen besteht, erleichtert einem das Verständnis. Nicht ohne Grund haben kleine, abgeschlossene Spezifikationsformen – wie Use Cases und User Stories – dem typischen Endlos-Spezifikationsfließtext den Rang abgelaufen.

Doch wir können das Prinzip sogar über die Softwareentwicklung hinaus – als allgemeines Bild für menschliches Denken und Handeln – verstehen. Dazu muss ich einen ehemaligen Professor zitieren, der mit der folgenden Formulierung eine wichtige Formel auf der Tafel ankündigte: "Bitte lenken Sie Ihren winzigen Suchstrahl der Aufmerksamkeit nun auf das folgende…". Der genannte Suchstrahl ist für mich die ideale Metapher für die menschliche Aufmerksamkeit. Sehr hell, aber auf einen kleinen Umkreis begrenzt. Menschen sind in der Lage, sehr komplexe Probleme zu lösen – wenn sie sich fokussieren können. Doch während auf der Bühne das "Spotlight" nur für den jeweils wichtigsten Schauspieler der Szene reserviert ist, ist unsere Aufmerksamkeit im Alltag stets der Konkurrenz verschiedenster Dinge ausgesetzt. Permanentes gedankliches Multitasking verringert unsere Konzentrationsfähigkeit. Es macht demnach auch für die Denkarbeit im Allgemeinen Sinn, diese Erkenntnis zu beherzigen: Artefakte zuerst nach akut wichtigen Themen sortieren und dann einzeln nacheinander abarbeiten. Das erscheint gleichermaßen trivial, wie es in der Praxis nicht eingehalten wird. Denn dann wäre es vollkommen normal, dass ein Entwickler auch einmal nicht erreichbar ist, oder nicht für einen Notfalleinsatz zur Verfügung steht, weil er gerade konzentriert eine "harte Nuss" knacken muss. Und alle Meetings hätten ein einfaches, klares Ziel, das am Ende auch schnell und widerspruchsfrei festgehalten werden kann. Sind wir da schon? Nein? Dann können wir im Arbeitsalltag sicher noch davon profitieren, unsere Zeit und Aufmerksamkeit nach dem "Single Responsibility Principle" zu optimieren.

Quellen
  1. Informatik Aktuell – Andre Krämer: Das Dependency Inversion Principle
  2. Informatik Aktuell – Andre Krämer: Das Single Responsibility Principle

Autor

Andre Krämer

Andre Krämer ist langjähriger Software-Architekt, -Trainer und -Berater mit den Schwerpunkten Microsoft.Net und C++. Sein Fokus liegt in der Entwicklung komplexer und performancekritischer Datenverarbeitungs- und...
>> Weiterlesen
Das könnte Sie auch interessieren
botMessage_toctoc_comments_9210