Warum moderne Sprachen Vererbung minimieren und was an ihre Stelle tritt
SerieSprachtheorie für Praktiker
Teil 5 von 6
„Favor composition over inheritance” — dieses Prinzip aus dem Gang-of-Four-Buch von 1994 hat sich durchgesetzt. Aber nicht nur als Best Practice: Moderne Sprachen bauen es direkt ins Sprachdesign ein.
Go hat keine Klassen. Rust hat keine Vererbung. Swift nutzt Protocols statt abstrakter Klassen. Kotlin bevorzugt Delegation. Selbst Java, die Hochburg der OOP, führt Sealed Classes und Records ein.
Die Frage ist nicht mehr, ob Vererbung ein Problem ist. Die Frage ist, was an ihre Stelle tritt — und warum die Alternativen nicht nur anders, sondern besser sind.
Warum Vererbung scheitert
Vererbung hat drei strukturelle Probleme, die sich nicht durch „besser designen” lösen lassen.
Das Fragile-Base-Class-Problem
// Version 1
class Counter {
int count = 0;
void add() { count++; }
void addMany(int n) { for (int i = 0; i < n; i++) add(); }
}
// Abgeleitete Klasse
class LoggingCounter extends Counter {
@Override
void add() {
log("Adding 1");
super.add();
}
}
// Version 2 - Base Class wird optimiert
class Counter {
int count = 0;
void add() { count++; }
void addMany(int n) { count += n; } // Optimierung!
}
Plötzlich loggt LoggingCounter.addMany() nicht mehr. Die Basisklasse hat sich geändert, ohne den Vertrag explizit zu brechen — aber das Verhalten der abgeleiteten Klasse ist kaputt. Das ist kein Designfehler. Das ist ein Strukturproblem: Vererbung koppelt Implementierungsdetails über Klassengrenzen hinweg.
Hierarchien bilden die Welt nicht ab
Wo gehört eine Fledermaus hin? Ein Säugetier, das fliegt. Wo ein fliegender Fisch? Die Welt lässt sich nicht in Bäume pressen — Fähigkeiten schneiden quer durch Kategorien. Jede Hierarchie, die heute passt, wird morgen zum Hindernis.
Erzwungene Abhängigkeiten
class EmailService extends BaseService {
// Braucht nur Logging, bekommt aber auch:
// - Database connection
// - Cache
// - Metrics
// - Auth context
// ... weil BaseService alles hat
}
Vererbung erzwingt Abhängigkeiten, die nicht benötigt werden. Das Liskov’sche Substitutionsprinzip wird zum Lippenbekenntnis, wenn die Basisklasse mehr mitbringt als die Subklasse je nutzen wird.
Composition: Explizit statt implizit
Der klassische Gegenentwurf — Objekte enthalten andere Objekte statt von ihnen zu erben:
class EmailService {
constructor(
private readonly logger: Logger,
private readonly smtp: SmtpClient,
private readonly templates: TemplateEngine
) {}
async sendWelcome(user: User): Promise<void> {
this.logger.info(`Sending welcome to ${user.email}`);
const html = this.templates.render('welcome', { user });
await this.smtp.send({ to: user.email, html });
}
}
Jede Abhängigkeit ist sichtbar, testbar, austauschbar. Kein versteckter State aus einer Basisklasse, keine Überraschungen bei Änderungen. Der Preis: etwas mehr Boilerplate. Der Gewinn: Klarheit über das, was tatsächlich passiert.
Traits: Verhalten ohne Abstammung (Rust)
Rust geht einen Schritt weiter. Traits definieren Fähigkeiten, die ein Typ haben kann — ohne Vererbungshierarchie:
trait Drawable {
fn draw(&self);
}
trait Clickable {
fn on_click(&self);
}
struct Button {
label: String,
}
impl Drawable for Button {
fn draw(&self) {
println!("Drawing button: {}", self.label);
}
}
impl Clickable for Button {
fn on_click(&self) {
println!("Button clicked!");
}
}
Ein Typ kann beliebig viele Traits implementieren. Kein Vererbungsbaum, keine Hierarchie, kein Diamond Problem. Und das Entscheidende: Traits können Default-Verhalten mitbringen, ohne die Probleme der Vererbung zu erben.
trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("Read more from {}...", self.summarize_author())
}
}
struct Article {
author: String,
content: String,
}
impl Summary for Article {
fn summarize_author(&self) -> String {
self.author.clone()
}
// summarize() nutzt die Default-Implementation
}
Das ist Code-Wiederverwendung ohne Kopplung. Der Typ entscheidet, welches Verhalten er übernimmt und welches er überschreibt — aber die Abhängigkeit ist explizit und lokal, nicht über eine Hierarchie verteilt.
Protocols: Composition auf Sprachebene (Swift)
Swift’s Protocols sind konzeptionell ähnlich, gehen mit Protocol Extensions aber noch weiter:
protocol Identifiable {
var id: String { get }
}
protocol Timestamped {
var createdAt: Date { get }
}
extension Identifiable where Self: Timestamped {
var uniqueKey: String {
"\(id)-\(createdAt.timeIntervalSince1970)"
}
}
struct User: Identifiable, Timestamped {
let id: String
let createdAt: Date
let name: String
}
let user = User(id: "123", createdAt: Date(), name: "Alice")
print(user.uniqueKey) // Automatisch verfügbar
Die Extension fügt Verhalten hinzu, das nur existiert, wenn ein Typ beide Protocols implementiert. Das ist Composition auf Sprachebene — keine Vererbung, keine Klassen, trotzdem mächtiger Polymorphismus.
Interfaces + Embedding: Go’s pragmatischer Weg
Go hat keine Klassen, keine Vererbung, keine Generics (bis vor kurzem). Stattdessen: Interfaces und Struct Embedding.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Composition von Interfaces
type ReadWriter interface {
Reader
Writer
}
// Struct Embedding
type BufferedWriter struct {
Writer // Embedded - alle Methoden werden delegiert
buffer []byte
}
Das sieht aus wie Vererbung, ist aber Delegation mit syntaktischem Zucker. Der Unterschied: BufferedWriter ist kein Writer im Sinne einer Hierarchie. Es hat einen Writer — und leitet dessen Methoden weiter. Saubere Trennung, auch wenn die Syntax etwas anderes suggeriert.
Go’s implizite Interface-Implementierung verstärkt den Effekt: Ein Typ implementiert ein Interface, indem er die richtigen Methoden hat — ohne explizite Deklaration. Das entkoppelt Interface-Definition und -Implementierung vollständig.
Mixins: TypeScripts Mittelweg
TypeScript hat weder Traits noch Protocols, aber das Mixin-Pattern ermöglicht ähnliche Composition:
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
updatedAt = new Date();
touch() {
this.updatedAt = new Date();
}
};
}
function Tagged<TBase extends Constructor>(Base: TBase) {
return class extends Base {
tags: string[] = [];
addTag(tag: string) {
this.tags.push(tag);
}
};
}
class Article {
constructor(public title: string) {}
}
const TrackedArticle = Tagged(Timestamped(Article));
const article = new TrackedArticle("Hello");
article.touch();
article.addTag("news");
Funktioniert, ist aber weniger elegant als Traits oder Protocols. Das Muster nutzt Vererbung intern — es versteckt sie nur hinter Funktionsaufrufen. Für TypeScript-Projekte ist der pragmatischere Weg oft: Interfaces für Polymorphismus, Composition für Abhängigkeiten, und Mixins nur für Cross-Cutting Concerns.
Das Muster: Fähigkeiten statt Abstammung
Was alle Alternativen gemeinsam haben: Sie beantworten eine andere Frage.
| Konzept | Frage | Beispiel |
|---|---|---|
| Vererbung | Was ist es? | Ein Hund ist ein Säugetier ist ein Tier |
| Traits / Protocols | Was kann es? | Es kann laufen, bellen, schwimmen |
Die zweite Frage ist für Software fast immer relevanter. Ein EmailSender muss nicht von BaseSender erben — er muss Sendable implementieren. Ein User muss nicht von Persistable erben — er muss die richtigen Methoden haben.
Dieses Umdenken zieht sich durch alle modernen Sprachen:
- Rust: Traits als einziger Polymorphismus-Mechanismus
- Go: Implizite Interfaces, kein
extends - Swift: Protocol-Oriented Programming als offizielles Paradigma
- Kotlin:
data class,sealed class, Delegation statt Vererbung - TypeScript: Structural Typing — ein Objekt erfüllt ein Interface, wenn es die richtige Form hat
Was von OOP bleibt
Objektorientierung ist nicht tot — aber ihre Definition hat sich verschoben.
Kapselung bleibt wichtig. Private Felder, Module, klar definierte Schnittstellen — das ist nicht verhandelbar.
Polymorphismus lebt weiter, aber über Interfaces und Traits statt über Vererbungshierarchien. Der Effekt ist derselbe, die Kopplung ist geringer.
Vererbung wird zum Spezialwerkzeug. Sinnvoll bei echten „ist-ein”-Beziehungen (Exceptions, UI-Widgets in Framework-Kontexten), aber nicht als Standardmechanismus für Code-Wiederverwendung.
Abstraktion bleibt zentral — sie wird nur anders ausgedrückt.
Alan Kay, der den Begriff „Object-Oriented” prägte, betonte immer Messaging zwischen Objekten, nicht Hierarchien. In gewisser Weise kehren moderne Sprachen zu dieser ursprünglichen Vision zurück — nur mit besseren Werkzeugen.
Unterm Strich
Wer heute ein neues Projekt startet, sollte Vererbung als letztes Mittel behandeln, nicht als erstes. Composition für Abhängigkeiten. Interfaces oder Traits für Polymorphismus. Und die Frage „Was kann dieser Typ?” statt „Von wem erbt er?”
Die Sprachen machen es vor. Go und Rust beweisen, dass man ohne Vererbung produktiver sein kann, nicht weniger. Swift und Kotlin zeigen, dass Protocols und Delegation die meisten Anwendungsfälle besser abdecken. Selbst Java bewegt sich in diese Richtung — langsam, aber unaufhaltsam.
Der Tod der Vererbung ist kein Verlust. Es ist die Korrektur eines Missverständnisses, das drei Jahrzehnte lang als Paradigma durchging.