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:
| Buchstabe | Prinzip | Einzeiliges Ziel |
|---|---|---|
| S | Single Responsibility | Eine Klasse sollte einen Grund zur Änderung haben |
| O | Open/Closed | Offen für Erweiterung, geschlossen für Modifikation |
| L | Liskov Substitution | Subtypen müssen überall dort verwendbar sein, wo ihr Basistyp erwartet wird |
| I | Interface Segregation | Viele kleine Interfaces schlagen ein großes fettes |
| D | Dependency Inversion | Von 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 Notifier – AlertService 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.
Was man aus der Ausführung mitnehmen kann:
email sent: [EMAIL -> alice: 3 error(s) detected]enthält nur einen Eintrag –bobhatte null Fehler, also hatraisenichts gesendet.AlertServiceträ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 einenEmailNotifierals auch einenSmsNotifier, weil die Abhängigkeit über den Konstruktor übergeben wurde (DIP). Die Alarmierungslogik auf hoher Ebene hängt nur vomNotifier-Interface ab, nie von einem konkreten Sender. OCP check : ... unchanged = truebestätigt, dass beide Alert-Objekte die gleicheAlertService-Klasse sind: Das Hinzufügen von SMS-Unterstützung bedeutete das Schreiben eines neuenSmsNotifier, ohne Änderungen anAlertService– offen für Erweiterung, geschlossen für Modifikation.ISP check : is Writable? falsezeigt, dassConfigFilenurReadableimplementiert. Da die Interfaces getrennt sind, wurde die nur-lesende Quelle nie gezwungen, einen bedeutungslosenwrite-Stub bereitzustellen.LSP area : 9.142ist die Summe eines 2×3-Rechtecks (6,0) und eines Kreises mit Radius 1 (≈3,142).totalAreaiterierte überShape-Referenzen und riefarea()auf, ohne zu prüfen, welchen Subtyp es hielt – jeder Subtyp war durch seine Basis substituierbar (LSP).