W3docs

Java SOLID-Prinzipien

Die SOLID-Prinzipien – SRP, OCP, LSP, ISP, DIP – in Java-Design anwenden.

SOLID ist ein Satz von fünf objektorientierten Entwurfsprinzipien – popularisiert von Robert C. Martin –, die Java-Code einfach zu ändern, zu testen und zu erweitern halten, während er wächst. Es sind keine Syntaxregeln, die der Compiler erzwingt; es sind Richtlinien dafür, wo man Grenzen zwischen Klassen ziehen soll, sodass eine Änderung nicht durch die gesamte Codebasis hallt. Das Akronym steht für Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation und Dependency Inversion.

Diese Prinzipien bauen auf den objektorientierten Grundlagen auf: Sie werden überall Interfaces, Vererbung und Polymorphismus sehen. Wenn diese noch unsicher wirken, lesen Sie sie zuerst nach – SOLID ist vor allem gutes Urteilsvermögen darüber, wo man sie anwendet.

Die fünf Prinzipien auf einen Blick

Jeder Buchstabe adressiert eine bestimmte Art von Designproblem. Halten Sie diese Tabelle bereit, während Sie den Rest des Kapitels lesen:

BuchstabePrinzipEinzeiliges Ziel
SSingle ResponsibilityEine Klasse sollte einen Grund zur Änderung haben
OOpen/ClosedOffen für Erweiterung, geschlossen für Modifikation
LLiskov SubstitutionSubtypen müssen überall dort verwendbar sein, wo ihr Basistyp erwartet wird
IInterface SegregationViele kleine Interfaces schlagen ein großes fettes
DDependency InversionVon Abstraktionen abhängen, nicht von konkreten Klassen

Die Prinzipien verstärken sich gegenseitig. In gut strukturiertem Code wenden Sie selten nur eines an – ein kleines Interface (ISP), von dem Code auf hoher Ebene abhängt (DIP), ist genau das, was es ermöglicht, eine neue Implementierung hinzuzufügen (OCP), ohne den Aufrufer zu berühren.

S — Single Responsibility Principle

Eine Klasse sollte eine Sache tun und einen Grund zur Änderung haben. Wenn nicht zusammenhängende Belange – z. B. Geschäftsregeln und Nachrichtenübermittlung – eine Klasse teilen, zwingt eine Änderung an einem Teil dazu, beide neu zu testen. Das Aufteilen isoliert Änderungen.

// Mixes WHEN to alert with HOW to deliver -- two reasons to change.
class BadAlertService {
  void raise(String user, int errors) {
    if (errors > 0) {
      // ...build an email, open an SMTP connection, send...
    }
  }
}

// One responsibility: deciding when to alert. Delivery lives elsewhere.
class AlertService {
  private final Notifier notifier;
  AlertService(Notifier notifier) { this.notifier = notifier; }
  void raise(String user, int errors) {
    if (errors > 0) notifier.send(user, errors + " error(s) detected");
  }
}

O — Open/Closed Principle

Softwareentitäten sollten offen für Erweiterung, aber geschlossen für Modifikation sein. Sie sollten in der Lage sein, neues Verhalten hinzuzufügen, indem Sie neuen Code schreiben, nicht indem Sie bereits funktionierenden Code bearbeiten – und riskieren. In Java ist der übliche Hebel ein stabiles Interface plus neue Implementierungen.

interface Notifier { void send(String to, String message); }

class EmailNotifier implements Notifier { /* ... */ }
class SmsNotifier   implements Notifier { /* ... */ } // new feature = new class

// AlertService never changes when a new channel appears.

Das spätere Hinzufügen von Push-Benachrichtigungen bedeutet das Schreiben von PushNotifier implements NotifierAlertService bleibt unberührt, benötigt also keine erneute Prüfung und birgt kein Regressionsrisiko.

L — Liskov Substitution Principle

Wenn S ein Subtyp von T ist, können Objekte vom Typ T durch Objekte vom Typ S ersetzt werden, ohne das Programm zu beeinträchtigen. Eine Unterklasse muss den Vertrag ihrer Elternklasse einhalten – gleiche Erwartungen, keine überraschenden Ausnahmen, keine strengeren Vorbedingungen.

abstract class Shape { abstract double area(); }
class Rectangle extends Shape { /* area() = w * h */ }
class Circle    extends Shape { /* area() = PI * r * r */ }

// Works for ANY Shape, present or future, without inspecting the concrete type.
double totalArea(List<Shape> shapes) {
  return shapes.stream().mapToDouble(Shape::area).sum();
}

Die klassische Verletzung ist Square extends Rectangle: Wenn das Setzen der Breite auch die Höhe verändert, bricht Code, der für ein Rectangle geschrieben wurde, wenn er ein Square erhält. Die Lösung besteht darin, sie als Geschwister unter Shape zu modellieren, nicht als Eltern-Kind-Paar. (Siehe abstrakte Klassen für die hier verwendete Shape-Basis.)

I — Interface Segregation Principle

Clients sollten nicht gezwungen werden, von Methoden abzuhängen, die sie nicht verwenden. Bevorzugen Sie mehrere kleine, fokussierte Interfaces gegenüber einem großen – sonst wird ein Implementierer dazu verleitet, Methoden zu stubben, die er nicht einhalten kann.

// Fat interface: a read-only source is forced to implement write().
interface Storage { String read(); void write(String data); }

// Segregated: implement only what you can honor.
interface Readable { String read(); }
interface Writable { void write(String data); }

class ConfigFile implements Readable {        // no empty write() stub
  public String read() { return "mode=prod"; }
}

D — Dependency Inversion Principle

Module auf hoher Ebene sollten nicht von Modulen auf niedriger Ebene abhängen; beide sollten von Abstraktionen abhängen. In der Praxis: gegen Interfaces coden und die konkrete Implementierung injizieren (Constructor-Injection ist die einfachste Form). Das ist es, was die anderen Prinzipien zum Tragen bringt – und was eine Klasse testbar macht, da Sie ein Fake einschleusen können.

// AlertService depends on the Notifier interface, not EmailNotifier.
AlertService alerts = new AlertService(new EmailNotifier());
// In a test, inject a fake Notifier and assert on what it recorded.

Ein durchgängiges Beispiel: alle fünf in einem Programm

Dieses Programm verbindet die Prinzipien – ein einzelner AlertService (SRP) kommuniziert mit einem injizierten Notifier (DIP), wechselt zwischen einem EmailNotifier und einem SmsNotifier ohne Änderungen (OCP), liest eine nur Readable-ConfigFile (ISP) und summiert Flächen über Shape-Subtypen einheitlich (LSP). Es überprüft seine eigenen Ergebnisse, sodass Sie sehen können, wie jedes Prinzip greift.

java— editable, runs on the server

Was man aus der Ausführung mitnehmen kann:

  • email sent: [EMAIL -> alice: 3 error(s) detected] enthält nur einen Eintrag – bob hatte null Fehler, also hat raise nichts gesendet. AlertService trägt die einzige Verantwortung, zu entscheiden, wann ein Alert ausgelöst wird (SRP); er erstellt weder einen Nachrichtentext noch öffnet er eine Verbindung.
  • Die gleiche AlertService-Klasse steuerte sowohl einen EmailNotifier als auch einen SmsNotifier, weil die Abhängigkeit über den Konstruktor übergeben wurde (DIP). Die Alarmierungslogik auf hoher Ebene hängt nur vom Notifier-Interface ab, nie von einem konkreten Sender.
  • OCP check : ... unchanged = true bestätigt, dass beide Alert-Objekte die gleiche AlertService-Klasse sind: Das Hinzufügen von SMS-Unterstützung bedeutete das Schreiben eines neuen SmsNotifier, ohne Änderungen an AlertService – offen für Erweiterung, geschlossen für Modifikation.
  • ISP check : is Writable? false zeigt, dass ConfigFile nur Readable implementiert. Da die Interfaces getrennt sind, wurde die nur-lesende Quelle nie gezwungen, einen bedeutungslosen write-Stub bereitzustellen.
  • LSP area : 9.142 ist die Summe eines 2×3-Rechtecks (6,0) und eines Kreises mit Radius 1 (≈3,142). totalArea iterierte über Shape-Referenzen und rief area() auf, ohne zu prüfen, welchen Subtyp es hielt – jeder Subtyp war durch seine Basis substituierbar (LSP).

Übung

Übung
Eine Klasse namens ReportGenerator formatiert sowohl Berichtsdaten als auch schreibt diese auf die Festplatte, sodass jede Änderung an den Formatierungsregeln oder dem Dateilayout dazu zwingt, dieselbe Klasse zu ändern und neu zu testen. Welches SOLID-Prinzip verletzt dies am direktesten?
Eine Klasse namens ReportGenerator formatiert sowohl Berichtsdaten als auch schreibt diese auf die Festplatte, sodass jede Änderung an den Formatierungsregeln oder dem Dateilayout dazu zwingt, dieselbe Klasse zu ändern und neu zu testen. Welches SOLID-Prinzip verletzt dies am direktesten?
Was this page helpful?