14. Entwurfsmuster I – SOLID: I und D
Nach diesem Kapitel kannst du:
- ein zu breites Interface nach dem Interface Segregation Principle in kleine, gezielte Schnittstellen aufteilen.
- eine fest verdrahtete Abhängigkeit nach dem Dependency Inversion Principle durch eine hereingereichte Abstraktion ersetzen.
- DIP von Dependency Injection und von einem IoC-Container unterscheiden.
- erklären, wie die fünf SOLID-Prinzipien einander stützen.
In Kapitel 11 hast du die Muster-Idee und den SOLID-Überblick kennengelernt, in Kapitel 12 die Prinzipien S und O, in Kapitel 13 das L mit der Auswechslung. Jetzt folgen I und D, und das Fußballteam bleibt dabei. Beim I geht es um klare Rollen: Niemand verlangt vom Torhüter, dass er Elfmeter schießt, nur weil er auf der Mannschaftsliste steht. Beim D geht es um die Aufstellung: Der Spielzug ist für “einen Stürmer” geplant, und erst der Trainer entscheidet vor dem Anpfiff, wer diese Rolle heute spielt. Am Ende des Kapitels siehst du, wie alle fünf Prinzipien zusammenwirken.
Begriffe vorab
Abschnitt betitelt „Begriffe vorab“- Fat Interface: eine große, universelle Schnittstelle, die mehr Methoden vorschreibt, als einzelne Clients brauchen.
- High-Level-Modul: Code mit der eigentlichen Geschäftslogik, z. B. ein
OrderService. - Low-Level-Modul: Code mit konkreten technischen Details, z. B. ein Repository, das SQL an MySQL schickt.
- Dependency Injection: Abhängigkeiten werden von außen übergeben, statt dass eine Klasse sie selbst erzeugt (schon in Kapitel 11 eingeführt).
- IoC-Container: ein Werkzeug in Frameworks wie Angular, Spring oder NestJS, das die Injektion automatisiert.
- Composition Root: die eine Stelle im Programm, an der die konkreten Klassen gewählt und zusammengesteckt werden.
I: Kleine Schnittstellen statt einer großen
Abschnitt betitelt „I: Kleine Schnittstellen statt einer großen“Interface Segregation Principle (ISP). Viele kleine, gezielte (clientspezifische) Schnittstellen sind besser als eine große, universelle Schnittstelle. Kein Client sollte gezwungen werden, von Methoden abhängig zu sein, die er gar nicht verwendet.
Das Problem, das es löst: Große “Fat Interfaces” zwingen implementierende Klassen dazu, Methoden zu implementieren, die für sie gar keinen Sinn ergeben (oft mit leeren Methodenrümpfen oder geworfenen Exceptions gelöst).
// bad: a wide interface forces a meaningless implementationinterface Worker { work(): void; eat(): void;}
class HumanWorker implements Worker { work(): void { console.log("Arbeite..."); } eat(): void { console.log("Mache Mittagspause..."); }}
class RobotWorker implements Worker { work(): void { console.log("Arbeite..."); } eat(): void { throw new Error("Robots don't eat"); // forced to implement something meaningless }}RobotWorker wird durch das breite Worker-Interface gezwungen, eine für ihn sinnlose Methode zu implementieren.
// better: split into focused interfacesinterface Workable { work(): void;}
interface Eatable { eat(): void;}
class HumanWorker implements Workable, Eatable { work(): void { console.log("Arbeite..."); } eat(): void { console.log("Mache Mittagspause..."); }}
class RobotWorker implements Workable { work(): void { console.log("Arbeite..."); }}Beispielprojekt: 14_entwurfsmuster_solid_i_d_examples/ex01_isp_worker
classDiagram
accTitle: RobotWorker implementiert nur Workable, HumanWorker beide Interfaces
class Workable {
<<interface>>
+work() void
}
class Eatable {
<<interface>>
+eat() void
}
class HumanWorker
class RobotWorker
Workable <|.. HumanWorker
Eatable <|.. HumanWorker
Workable <|.. RobotWorker
RobotWorker hängt jetzt nur noch von Workable ab - einer Schnittstelle, die zu seinen tatsächlichen Fähigkeiten passt.
Genauer erklärt: Kleinere Interfaces reduzieren die Kopplung zwischen Client und Implementierung. Ein Client, der nur Workable kennt, ist von Änderungen an Eatable überhaupt nicht betroffen. Konkret heißt das: Fügt jemand Eatable eine Methode drink() hinzu, muss RobotWorker nicht angefasst werden, und sein Code muss nicht neu geprüft werden.
Wo begegnet dir das? In TypeScript schneidest du Typen ständig so zu: Eine Funktion, die nur den Titel anzeigt, verlangt { title: string } und nicht ein ganzes User- oder Task-Objekt mit zwanzig Feldern. Dasselbe Prinzip steckt in jeder Schnittstelle, die so klein ist, dass sie sich leicht durch eine Testattrappe ersetzen lässt.
D: Die Logik hängt von Abstraktionen ab
Abschnitt betitelt „D: Die Logik hängt von Abstraktionen ab“Dependency Inversion Principle (DIP). Es besteht aus zwei Teilen:
- High-Level-Module (die die eigentliche Geschäftslogik enthalten) sollen nicht von Low-Level-Modulen (konkreten technischen Details) abhängen - beide sollen von Abstraktionen abhängen.
- Abstraktionen sollen nicht von Details abhängen - Details sollen von Abstraktionen abhängen.
Der Name “Inversion” bezieht sich auf die Umkehrung der klassischen Abhängigkeitsrichtung: Normalerweise “benutzt” die Geschäftslogik eine konkrete Datenbank-Klasse und hängt damit direkt von ihr ab. DIP dreht das um.
// bad: OrderService (high-level) depends directly on a concrete classclass MySqlOrderRepository { save(order: Order): void { // SQL-specific code }}
class OrderService { private repository = new MySqlOrderRepository(); // fixed, concrete dependency
placeOrder(order: Order): void { this.repository.save(order); }}OrderService kennt MySqlOrderRepository direkt. Ein Wechsel der Speichertechnologie erfordert eine Änderung von OrderService selbst, und isoliertes Testen ohne echte Datenbank ist kaum möglich.
// better: both sides depend on an abstractioninterface OrderRepository { save(order: Order): void;}
class MySqlOrderRepository implements OrderRepository { save(order: Order): void { // SQL-specific code }}
class InMemoryOrderRepository implements OrderRepository { save(order: Order): void { // for tests }}
class OrderService { constructor(private repository: OrderRepository) {} // the abstraction is handed in
placeOrder(order: Order): void { this.repository.save(order); }}
// composition root: the concrete choice is made exactly once, hereconst service = new OrderService(new MySqlOrderRepository());Beispielprojekt: 14_entwurfsmuster_solid_i_d_examples/ex02_dip_order_repository
flowchart LR accTitle: Nach DIP zeigen alle Pfeile auf die Abstraktion OrderRepository S["OrderService"] -->|hängt ab von| R["OrderRepository (Interface)"] M["MySqlOrderRepository"] -->|implementiert| R I["InMemoryOrderRepository"] -->|implementiert| R
OrderService hängt jetzt nur noch von der Abstraktion OrderRepository ab. Im Fußballbild ist der Spielzug für “den Stürmer” geplant, und erst die Aufstellung, also der Composition Root, legt fest, wer heute spielt.
Genauer erklärt: Der Gewinn zeigt sich in drei Dingen: Testbarkeit (mit InMemoryOrderRepository statt echter Infrastruktur testen), Austauschbarkeit (Datenbank oder Zahlungsanbieter wechseln, ohne die Geschäftslogik anzufassen) und Schutz der Business-Logik vor technischen Detailänderungen. Es ist auch die Grundidee hinter Hexagonal Architecture / Ports & Adapters sowie Clean Architecture.
Wo begegnet dir das? Jedes Mal, wenn du in einem Test statt der echten Datenbank oder des echten Zahlungsdienstes eine Attrappe einsetzt. Auch in Angular, Spring und NestJS ist das die Grundlage: Das Framework reicht die Abhängigkeiten herein, du schreibst nur, wovon eine Klasse abhängt. Dieselbe Idee kommt in Kapitel 15 wieder, wenn das Singleton kritisiert wird.
Auflösung
Es verstößt gegen DIP: Die Geschäftslogik hängt direkt von zwei konkreten Klassen ab. Du definierst Interfaces (z. B. Exporter und Mailer), nimmst sie im Konstruktor entgegen und übergibst im Test eine Attrappe statt des SmtpMailer. Die konkrete Wahl trifft eine einzige Stelle, der composition root.
Zusammenspiel der fünf Prinzipien
Abschnitt betitelt „Zusammenspiel der fünf Prinzipien“Die fünf Prinzipien wirken oft zusammen statt isoliert:
- SRP sorgt für fokussierte, kleine Klassen - eine Voraussetzung dafür, dass OCP überhaupt sauber umsetzbar ist.
- OCP wird in der Praxis meist über Polymorphie erreicht - und Polymorphie funktioniert nur verlässlich, wenn LSP eingehalten wird.
- ISP hilft dabei, die Abstraktionen, die DIP verlangt, richtig zuzuschneiden: schlanke, clientspezifische Interfaces statt einer großen Abstraktion, die zu viel verspricht.
- DIP ist strukturell die Klammer: Es sorgt dafür, dass High-Level-Code stabil bleibt, während Low-Level-Details frei austauschbar sind.
flowchart LR accTitle: Wie die fünf SOLID-Prinzipien zusammenhängen SRP["SRP: kleine, fokussierte Klassen"] -->|Voraussetzung| OCP["OCP: erweitern ohne zu ändern"] LSP["LSP: Untertypen sind ersetzbar"] -->|macht verlässlich| OCP ISP["ISP: schlanke Interfaces"] -->|schneidet zu| DIP["DIP: von Abstraktionen abhängen"] DIP -->|hält stabil| H["High-Level-Code bleibt unverändert"]
In Summe zielen alle fünf Prinzipien auf dasselbe Ziel ab: Kopplung reduzieren, Verantwortlichkeiten klar trennen und Software so gestalten, dass Änderungen lokal bleiben.
Wo begegnet dir das? In einer guten Architekturdiskussion kommen die Prinzipien selten einzeln vor: “Die Klasse ist zu groß (S), deshalb zieht jede neue Variante einen Umbau nach sich (O).” Wer die Zusammenhänge kennt, findet die Ursache schneller als die Symptome.
Zusatzmaterialien
Abschnitt betitelt „Zusatzmaterialien“- Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern - vertieft, wie Abhängigkeiten hereingereicht werden und warum das Tests vereinfacht.
- Wikipedia: Dependency inversion principle - stellt die beiden Teile des Prinzips und ihre Umsetzung in Schichtenarchitekturen kompakt dar.
- Wikipedia: Interface segregation principle - lohnt sich, weil es zeigt, aus welchem realen Projektproblem das Prinzip entstanden ist.
- Wikipedia: SOLID - fasst alle fünf Prinzipien auf einer Seite zusammen, gut zum Wiederholen des Zusammenspiels.
Zum Anhören
Abschnitt betitelt „Zum Anhören“Du kannst dir diesen Text mit der Vorlesefunktion deines Browsers oder Betriebssystems kostenlos anhören, etwa mit “Laut vorlesen” in Microsoft Edge. Das eignet sich gut zum Wiederholen vor der Schularbeit.
Lernergebnisse: Was du nach diesem Kapitel können solltest
Abschnitt betitelt „Lernergebnisse: Was du nach diesem Kapitel können solltest“- Anwenden: ein zu breites Interface nach dem Interface Segregation Principle in kleine, gezielte Schnittstellen aufteilen.
- Anwenden: eine fest verdrahtete Abhängigkeit nach dem Dependency Inversion Principle durch eine hereingereichte Abstraktion ersetzen.
- Erklären: DIP von Dependency Injection und von einem IoC-Container unterscheiden.
- Analysieren: erklären, wie die fünf SOLID-Prinzipien einander stützen.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 09 - Jahresprojekt Sprint 2 - Review, ISP und DIP: Kata: ein zu breites Interface der Medienwerkstatt zerlegen, dazu Review und Retrospektive von Sprint 2 (Wesentlich_überwiegend); Release, Datenzugriff hinter ein Repository-Interface mit Composition Root und Implementierung im Arbeitsspeicher (Wesentlich_vollständig). PlusPlus: ISP im Projekt und zwei Konfigurationen; MandalorianPlus: DIP, DI und IoC einordnen, Zusammenspiel der fünf Prinzipien.
Autoren
Abschnitt betitelt „Autoren“GAM
CC-BY-SA