Zum Inhalt springen

14. Entwurfsmuster I – SOLID: I und D

Zu Zen-Modus wechseln

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.

  • 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.

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 implementation
interface 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 interfaces
interface 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.

Dependency Inversion Principle (DIP). Es besteht aus zwei Teilen:

  1. 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.
  2. 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 class
class 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 abstraction
interface 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, here
const 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.

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.

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.
  • 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.

GAM

CC-BY-SA

https://qiz.at/rules