SOLID – 5 Prinzipien des objektorientierten Designs

Inhaltsübersicht

SOLID-Prinzipien – Alles solide?

Das Akronym SOLID setzt sich aus den Anfangsbuchstaben von fünf Prinzipien der objektorientierten Programmierung zusammen:

  • SRP – Single Responsibility Principle
  • OCP – Open Closed Principle
  • LSP – Liskov Substitution Principle
  • ISP – Interface Segregation Principle
  • DIP – Dependency Inversion Principle

Ursprünglich in dieser Konstellation beschrieben wurden sie im Buch Agile Software Development, Principles, Patterns, and Practices von Bob C. Martin. Sie sind in diesem Buch allerdings in einer anderen Reihenfolge aufgeführt. Auf das Akronym kam später Michael Feathers, als er feststellte, dass eine andere Reihenfolge zu diesem Akronym führt.

Die 5 Prinzipien des objektorientierten Designs

Die SOLID Principles sind an der Objektorientierung ausgerichtet. Am deutlichsten wird das beim Liskovschen Substitutionsprinzip (LSP). Es macht Aussagen über die Vererbung. Die gibt es nur im objektorientierten Paradigma. Die anderen vier Prinzipien ergeben auch in den beiden anderen Paradigmen Sinn, der Funktionalen Programmierung sowie der Imperativen Programmierung, oft auch Prozedurale Programmierung genannt.


Single Responsibility Principle (SRP)

Ich halte das SRP für das fundamentale Prinzip der Softwareentwicklung. Es besagt:

Eine Funktionseinheit soll nur einen Grund für Änderungen haben.

Das mag anfangs etwas verwirrend klingen. Eine andere Definition wäre:

Eine Funktionseinheit darf nur eine einzige Verantwortlichkeit haben.

Doch an diese Definition schließt sich die Frage an, was ist eine Verantwortlichkeit (Responsibility)? Ich finde daher die erste Definition leichter umsetzbar. Sie ist näher an meinem täglichen Handeln als Softwareentwickler. Letztlich ergibt sich die Notwendigkeit für das SRP aus der Überlegung, dass Software wandelbar sein muss. Ständig kommen neue Anforderungen hinzu, die im Code umgesetzt werden müssen. Wandelbarkeit ist einer der vier Werte der Clean Code Developer Initiative. Damit Änderungen und Ergänzungen effizient möglich sind, darf jede Methode und jede Klasse nur einen Grund für Änderungen liefern. Damit wäre dann auch geklärt, was mit Funktionseinheit gemeint ist: Methoden und Klassen.

Sobald eine Methode mehr als einen Grund für Änderungen liefert, ist sie für mehr als eine Verantwortlichkeit zuständig. In dem Fall führen Änderungen aus unterschiedlichen Gründen zu einer Änderung an dieser Methode. Das führt zum einen dazu, dass dabei das Risiko höher ist, versehentlich auch an der anderen Zuständigkeit eine Änderung vorzunehmen. Vor allem ist eine Methode aber schwerer verständlich, wenn sie mehr als eine Sache macht. Und Dinge, die ich nicht verstehe, sollte ich eher nicht ändern.

Mit dem SRP ist es wie beim Segeln: wenn der Wind etwas auffrischt, fragt man sich, ob man reffen sollte (die Segelfläche verkleinern). Hier lautet die Faustregel: wenn du an’s Reffen denkst, tue es!

Als Faustregel kann ich daher zum SRP folgendes Empfehlen:

Wenn du im Zweifel bist, ob eine Methode nur eine Sache macht, teile sie weiter auf.

Dabei entsteht oft die Frage, ob Methoden dann nicht zu klein werden. Aus meiner langjährigen Erfahrung kann ich sagen, dass kleine Methoden bislang nie ein Problem dargestellt haben. Sie sind fokussiert und klar verständlich. Damit wird es leichter sie zu ändern und zu testen. Und sogar die Möglichkeit der Wiederverwendung wird erhöht.

Aus dem SRP ergibt sich auch für einen weiteren Wert ein Vorteil: der Wert der Korrektheit. Kleine Methoden mit klarer Zuständigkeit sind leichter automatisiert zu Testen, als lange, undurchschaubare Methoden, in denen mehrere Aspekte durcheinander gehen.


Open Closed Principle (OCP)

Die Definition sagt:

Eine Klasse soll offen sein für Änderungen, aber abgeschlossen gegenüber Modifikationen.

Bei dieser Definition ist der zweite Teil der relevante. Offen für Änderungen zu sein, ist nicht das Problem. Wenn sich neue oder geänderte Anforderungen ergeben, kann der Quellcode der betroffenen Klasse geändert werden. Insofern ist grundsätzlich jede Methode oder Klasse offen für Änderungen. Aber: der zweite Teil besagt, dass diese Änderungen ohne eine Modifikation der Methode oder Klasse selbst umsetzbar sein sollen. Der Quelltext soll nicht angerührt werden, um bestimmte Änderungen vorzunehmen.

Hier zeigt sich bereits: das OCP scheint nicht auf jede Klasse sinnvoll anwendbar. Die große Mehrheit der Klassen sind vom OCP nicht betroffen. Um offen für Erweiterungen zu sein, muss es eine Notwendigkeit für diese Flexibilität geben. Die bei weitem größte Menge an Klassen benötigt diese Flexibilität nicht.

Das OCP ergibt Sinn, wenn man Frameworks entwickelt, die in unterschiedlichen Szenarien zum Einsatz kommen sollen. Insbesondere zeichnen sich solche Frameworks dadurch aus, dass man die Verwender nicht alle kennt. Bei Klassen, die innerhalb eines Softwaresystems entwickelt werden, sind alle Verwender bekannt: das Softwaresystem! In dem Fall ist es bei Bedarf meist einfacher, die Klasse unmittelbar zu modifizieren, statt Abstraktionen einzuführen, die Änderungen des Verhaltens ohne Modifikation der Klasse ermöglichen würden.

Eines der wichtigsten Patterns zur Umsetzung des OCP ist das Strategy Pattern. Soll bspw. bei einer Klasse zur JSON Serialisierung von Objekten das verwendete Encoding änderbar sein, kann dieser Aspekt als Strategie in den Serialisierer reingereicht werden.

Im folgenden Beispiel wird über ein Objekt vom Typ JsonSerializerOptions ein Encoder definiert. Damit ist ein Verwender der Klasse JsonSerializer in der Lage, die Encodingstrategie von außen reinzureichen. So muss der Quellcode nicht angepasst werden. Als Entwickler könnte ich sogar hingehen und ein eigenes Encoding implementieren und verwenden.

				
					var options = new JsonSerializerOptions {
   Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.Cyrillic),
   WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, options);
				
			

Beim OCP ist es so, dass ich vor der Anwendung eigentlich eine Warnung aussprechen muss:

Wenn du im Zweifel bist, ob eine Klasse das OCP einhalten sollte, ignoriere es lieber.


Liskov Substitution Principle (LSP)

Die Definition sagt:

Bei der Vererbung darf die Funktionalität der Basisklasse nicht eingeschränkt werden.

Benannt nach Barbara Liskov, drückt das liskovsche Substitutionsprinzip aus, welche Spielregeln bei der Vererbung eingehalten werden müssen. Ein Verwender der Basisklasse macht bestimmte Annahmen über das Verhalten der Klasse. Da eine Ableitung syntaktisch kompatibel zum Basistyp ist, kann dem Verwender somit ein Objekt der abgeleiteten Klasse vorgelegt werden. Noch immer betrachtet der Verwender dies aber als Objekt der Basisklasse. Würde sich nun die Ableitung anders verhalten, würde die Vereinbarung mit dem Verwender gebrochen.

Ganz wichtig in dem Zusammenhang ist das Prinzip Favour Composition over Inheritance. Wenn eine bestehende Funktionalität wiederverwendet werden soll, muss zunächst geprüft werden, ob dies durch die Komposition möglich ist. Das folgende Pseudocodebeispiel soll dies verdeutlichen. Möchte die Klasse B bestehende Funktionalität aus der Klasse A verwenden, muss sie nicht zwingend davon ableiten. Stattdessen kann sie über die Komposition von Objekten Zugriff erhalten. B verwendet A, statt davon abzuleiten:

Aktuelle Clean Code Trainings

Clean Code Developer Basics | 3-tägig

Termine der einzelnen Trainingstage:

27.05.2025 / 17.06.2025 / 07.07.2025
Clean Code Developer Refactoring | 1-tägig

Termine der einzelnen Trainingstage:

03.06.2025

Wir führen alle Seminare auch als geschlossene Firmenkurse für Sie durch.
Bei Interesse oder Fragen kontaktieren Sie uns gerne.

				
					class A {
   public int Calculate(int[] values) { ... }
}

class B {
   private A _a = new A();

   public int Calculate(int[] values) {
      //...
      var otherValues = _a.Calculate(values);
      //...
   }
}
				
			

In manchen Fällen mag es dennoch sinnvoll sein, mit Vererbung zu arbeiten. Da durch Vererbung eine sehr enge Abhängigkeit zwischen den beteiligen Klassen hergestellt wird, muss dies wohlüberlegt sein. Bei Komponentenbibliotheken für grafische Benutzerschnittstellen mag das die beste Lösung sein. Bei Domänenlogik innerhalb einer Anwendung ist es eher selten sinnvoll auf Vererbung zu setzen.

Meine Empfehlung lautet daher:

Prüfe zunächst, ob die gewünschte Funktionalität auch durch Komposition hergestellt werden kann. Wenn die Vererbung sich als die beste Lösung herausstellt, beachte das LSP.


Interface Segregation Principle (ISP)

Die Definition sagt:

Interfaces sollten einen Verwender nicht in die Abhängigkeit von Details zwingen, die dieser nicht benötigt.

Die Umsetzung des Prinzips ist einfach: trenne die Schnittstellen nach Aspekten auf. Man könnte auch sagen, dass das ISP ein Spezialfall des SRP ist. Wenn eine Schnittstelle mehr als eine Verantwortlichkeit hat, verstößt sie gegen das ISP. Denn dann zwingt sie bei Verwendung des einen Aspekts auch in die Abhängigkeit zum anderen Aspekt.

Ein klassisches Beispiel dafür ist eine große Schnittstelle IMultiFunctionDevice die Methoden zum Drucken, Scannen und Faxen enthält. Wenn nun ein einfaches Gerät nur drucken kann, muss es trotzdem die gesamte Schnittstelle implementieren und Methoden für Scannen und Faxen bereitstellen – selbst wenn diese nichts tun. Das widerspricht dem ISP.

Besser wäre es, die Schnittstellen aufzuteilen:

				
					interface IPrinter {
   void Print(Document doc);
}

interface IScanner { 
   void Scan(Document doc);
}

interface IFax {
   void Fax(Document doc);
}
				
			

Ein Gerät, das nur drucken kann, implementiert dann nur IPrinter. Dadurch wird es nicht gezwungen, unnötige Implementierungen vorzuhalten. Auch Verwender werden dann nicht an Funktionalität gebunden, die sie nicht benötigen.

Die Einhaltung des ISP führt zu klaren Schnittstellen und geringer Kopplung. Einzelne Module werden robuster, weil sie sich nicht ändern müssen, wenn sich eine andere Verantwortung innerhalb einer ehemals gemeinsamen Schnittstelle ändert.

Meine Empfehlung:

Wenn du bei der Definition einer Schnittstelle das Gefühl hast, dass nicht jeder Verwender alle Methoden braucht – dann splitte die Schnittstelle auf.


Dependency Inversion Principle (DIP)

Die Definition lautet:

High-level Module sollen nicht von low-level Modulen abhängig sein. Beide sollen abhängig sein von Abstraktionen.

Abstraktionen sollen nicht von Details abhängig sein, sondern Details von Abstraktionen.

Dieses Prinzip ist vermutlich das am häufigsten missverstandene der fünf SOLID-Prinzipien. Es besagt nicht, dass es keine Abhängigkeiten geben darf – im Gegenteil: Abhängigkeiten sind notwendig, aber sie sollen sich in die „richtige Richtung“ bewegen – weg von konkreten Implementierungen und hin zu Abstraktionen, meist in Form von Interfaces. Ferner wird das DIP leider immer noch zu selten hinterfragt. Es gibt mit dem IOSP eine Alternative!

Statt direkte Abhängigkeiten zwischen konkreten Klasse zuzulassen, sollten diese laut DIP nur von einem Interface abhängig sein. Die eine Klasse verwendet das Interface, die andere implementiert es. Die konkrete Implementierung wird dann über Dependency Injection zur Laufzeit eingebunden.

Dazu ein Beispiel:

				
					interface IEmailService {
   void SendEmail(string to, string subject, string body);
}

class OrderService {
   private readonly IEmailService _emailService;

   public OrderService(IEmailService emailService) {
      _emailService = emailService;
   }

   public void PlaceOrder(Order order) {
      // Bestellung verarbeiten...
      _emailService.SendEmail(order.CustomerEmail, "Bestätigung", "Danke für Ihre Bestellung!");
   }
}
				
			

Die Klasse OrderService hängt nicht von einer konkreten Implementierung des E-Mail-Versands ab, sondern von der Abstraktion IEmailService. Dadurch ist sie flexibel, leicht testbar und nicht von technischen Details abhängig.

Allerdings vermischt die Klasse OrderService Aspekte, da sie sich zum einen um die Abwicklung der Bestellung kümmert und zum anderen den Emailversand anstößt. Dieses Problem kann durch das IOSP gelöst werden.

Meine Empfehlung:

Wenn du eine neue Klasse schreibst, frage dich: Hängt sie direkt von konkreten Implementierungen ab? Wenn ja, ziehe ein Interface ein und injiziere die Abhängigkeit von außen.

Noch besser ist allerdings die Berücksichtigung des Integration Operation Segregation Principles (IOSP).

Sauberen Code schreiben durch SOLID?

Die SOLID-Prinzipien erscheinen häufig als die zentralen Prinzipien für Clean Code. Doch in meiner Beobachtung führen sie nicht zwingend zu sauberem Code. Das liegt daran, dass unter den 5 Prinzipien nur eines wirklich relevant ist: das SRP. Die anderen 4 Prinzipien stehen auf einer völlig anderen Ebene und sind bei weitem nicht so zentral wie das SRP.

  • OCP: führt häufig zu nicht benötigter Generalisierung und zu viel Abstraktion
  • LSP: nur relevant bei Vererbung, die ohnehin vermieden werden sollte
  • ISP: wichtig, aber kein Gamechanger
  • DIP: führt zu vielen Interfaces und Tests mit Attrappen. Überholt durch das IOSP.

Sind die SOLID Prinzipien ausreichend?

Nein, das sind sie ganz gewiss nicht. Wie im vorherigen Abschnitt dargestellt ist eigentlich nur das SRP relevant. Wenn die anderen 4 Prinzipien eher nicht so zentral sind, können die SOLID Principles nicht ausreichend sein.

Welche zusätzlichen Prinzipien sind wichtig?

Ich halte die folgenden Prinzipien für wichtig als Ergänzung zum SRP:

  • IOSP – Integration Operation Segregation Principle
    Das zentrale Prinzip zum Umgang mit Abhängigkeiten. Es löst das DIP in weiten Teilen ab.
  • DRY – Don’t Repeat Yourself
    Dopplung im Code ist oft der Grund für Fehler und erschwert die Wandelbarkeit. Gleichzeitig führt das Herausziehen von gemeinsamem Code manchmal zu Abhängigkeiten, die dann ebenfalls die Wandelbarkeit erschweren. Mit Augenmaß angewendet, hilft das DRY Prinzip.
  • YAGNI – You ain’t gonna need it
    Setze nur um, was sich aus den Anforderungen ergibt. Das führt schneller zu Feedback und reduziert den Aufwand im Code. Oft wird der Code dadurch auch einfacher, weil auf Sonderlocken verzichtet wird, die eh nicht gefordert waren.
  • KISS – Keep it simple stupid
    Implementiere auf die einfachstmögliche Weise. Auch hier gilt: keine Sonderlocken. Wirklich gute Entwickler demonstrieren nicht, dass sie alle Patterns beherrschen und die tollsten Abstraktionen bauen können. Stattdessen implementieren sie die konkreten Anforderungen auf einfache und elegante Weise.

Fazit

Sicher kann man über die Auswahl der Prinzipien diskutieren. Am Ende geht es mir nicht um die eine richtige Auswahl. Zentral ist für mich der Gedanke, dass die SOLID Prinzipien garantiert NICHT die Rolle spielen, die sie durch das Akronym vorgeben.

Unsere Seminare

course
Clean Code Developer Basics

Prinzipien und Tests – Das Seminar wendet sich an Softwareentwickler, die gerade beginnen, sich mit dem Thema Softwarequalität auseinanderzusetzen. Es werden die wichtigsten Prinzipien und Praktiken der Clean Code Developer Initiative vermittelt.

zum Seminar »
course
Clean Code Developer Advanced

Mit Flow Design von den Anforderungen zum Clean Code – Lernen Sie mit Flow Design einen Softwareentwicklungsprozess kennen, der Sie flüssig von den Anforderungen zum Clean Code führt.

zum Seminar »
course
Clean Code Developer Trainer

Seminare als Trainer durchführen – Dieses Seminar wendet sich an Softwareentwickler, die ihr Wissen über die Clean Code Developer Prinzipien und Praktiken bzw. über Flow Design als Trainer an andere weitergeben möchten.

zum Seminar »

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

de_DEGerman