Komposition statt Vererbung: Objekte flexibel zusammenbauen
Kurz: Statt Verhalten über tiefe Vererbungsbäume zu teilen, setzt man Objekte aus kleinen Bausteinen zusammen; das ist flexibler, leichter zu testen und vermeidet starre Hierarchien.
Genauer: Vererbung drückt „ist ein“ aus (ein Hund ist ein Tier), Komposition „hat ein“ oder „nutzt ein“ (ein Auto hat einen Motor). Eine bekannte Faustregel des Entwurfs lautet: Komposition vor Vererbung bevorzugen.
Im Detail
Das Problem mit Vererbung
- Starre Bindung: Die Unterklasse hängt von Interna der Oberklasse ab; Änderungen oben brechen unten (Fragile-Base-Class-Problem).
- Explodierende Hierarchien: Nach Tier → Vogel → FliegenderVogel → NichtFliegenderVogel (Pinguin, Strauß) wird es unübersichtlich; Kombinationen (Schwimmen, Fliegen, Laufen) führen zu vielen Klassen.
- Verletzte Erwartungen (Liskov): Ein Quadrat als Unterklasse von Rechteck bricht Annahmen.
- Nur eine Oberklasse in vielen Sprachen (Java, C#).
Komposition im Beispiel
class Fliegen:
def bewegen(self): return "flattert"
class Laufen:
def bewegen(self): return "läuft"
class Vogel:
def __init__(self, bewegung):
self.bewegung = bewegung # hat ein Verhalten
def bewege_dich(self):
return self.bewegung.bewegen()
spatz = Vogel(Fliegen())
pinguin = Vogel(Laufen())Das Verhalten ist austauschbar (auch zur Laufzeit) und wird über Schnittstellen angesprochen. Das ist das Strategie-Muster; ähnlich arbeiten Dekorierer, Delegation und Dependency Injection.
Wann Vererbung passt
Wenn es wirklich eine „ist ein“-Beziehung gibt, die stabil ist, und die Unterklasse alle Eigenschaften der Oberklasse sinnvoll erfüllt (Liskov-Substitutionsprinzip). Auch Schnittstellen/Interfaces und Traits/Mixins bieten sichere Wiederverwendung. Rust und Go haben bewusst keine klassische Vererbung und setzen auf Komposition und Traits/Interfaces.
Praktische Regeln
- Frage: Ist das wirklich ein Spezialfall, oder will ich nur Code wiederverwenden? Für Letzteres Komposition.
- Kleine, fokussierte Klassen mit einer Aufgabe (Single Responsibility).
- Schnittstellen statt konkreter Klassen als Abhängigkeit.
- Hierarchien flach halten (maximal 2 bis 3 Ebenen).
- Tests werden einfacher, weil man Bausteine einzeln ersetzen (mocken) kann.
Siehe auch: Polymorphie, SOLID-Prinzipien, Strategie-Muster, Dependency Injection, Abstrakte Klasse oder Interface