Veränderbarer Code: Vererbung, Polymorphie, SOLID und fremde Systeme übernehmen
Praktischer Workshop zum Entwickeln und Übernehmen von Code: Vererbung sinnvoll einsetzen, Polymorphie gestalten, SOLID ohne Übertreibung nutzen und unbekannte Systeme sicher refaktorieren.
Der vollständige Artikel steht unten. Aktivieren Sie JavaScript für Quiz und Simulationen.
Praktischer Architekturworkshop
Code ist erst gut, wenn die nächste Änderung keinen Mut erfordert.
Wir übernehmen ein unbekanntes Deployment-Prüfmodul. Zuerst rekonstruieren wir seinen Vertrag, trennen Vererbung von Komposition, nutzen Polymorphie und führen die Änderung durch Tests und Produktionsabnahme.
Am einfachsten ist Code, den noch niemand nutzt. Schwieriger ist eine Funktion in einem System, das Umsatz erzielt, Nachrichten sendet, Zahlungen verarbeitet und jahrelange Entscheidungen unter Zeitdruck mitbringt.
Wir behandeln keine bloße Definitionssammlung, sondern einen konkreten Fall: Du übernimmst die Deployment-Prüfung. Es gibt Basisklassen, überschriebene Methoden, einen großen switch, Exceptions als Normalablauf und Tests, die hauptsächlich Mocks prüfen. Du sollst eine Sicherheitsprüfung ergänzen, ohne versehentlich Produktion zu stoppen.
1. Eine kleine Änderung in unbekanntem Code
Die Aufgabe: „Vor dem Deployment prüfen, ob Produktionssecrets aus dem Vault stammen.“ Du findest die Klasse ReleaseCheckBasemit acht Unterklassen. Eine überschreibt eine Methode und gibt nullzurück, eine andere wirft NotSupportedException, die dritte verlangt ein gesetztes geschütztes Feld in der Basisklasse.
Was Konsumenten tatsächlich voraussetzen, nicht nur was einmal geplant war.
Datenbank, Netzwerk, Dateien, Queue, Cloud, E-Mail und Telemetrie.
Fachliches Verhalten oder nur die Reihenfolge von Mock-Aufrufen?
Adapter, kleines Interface, Handler oder Feature Flag statt großem Rewrite.
Entscheidung 1 von 4
Was tust du zuerst?
Zwei Stunden, ein unbekanntes Modul und eine Produktionsaufgabe.
Ich ordne Ordner und Namen
Ich rekonstruiere den Ablauf und sichere das Verhalten
Ich schreibe das Modul neu
Wähle einen Schritt und prüfe die Konsequenz.
2. Vererbung ist eine starke Vereinbarung
Vererbung bedeutet mehr als gemeinsamen Code. Die Unterklasse übernimmt Lebenszyklus, Zustand, Annahmen und künftige Änderungen der Basisklasse. Frage deshalb nicht nur nach gemeinsamen Feldern, sondern ob jede Unterklasse überall überraschungsfrei anstelle des Basistyps einsetzbar ist.
public abstract class FileStore
{
public abstract Task<byte[]> Read(string key, CancellationToken ct);
public abstract Task Save(string key, byte[] data, CancellationToken ct);
}
public sealed class ReadOnlyArchiveStore : FileStore
{
public override Task<byte[]> Read(string key, CancellationToken ct)
=> archive.Read(key, ct);
public override Task Save(string key, byte[] data, CancellationToken ct)
=> throw new NotSupportedException("Archiwum jest tylko do odczytu.");
}
ReadOnlyArchiveStore ist kein vollwertiger FileStore, wenn Teile des Basisversprechens nicht gelten. Der Aufrufer muss dann konkrete Typen kennen oder Exceptions abfangen; Polymorphie bleibt nur scheinbar bestehen.
Lesen, Schreiben und Löschen gelten für jede Unterklasse, auch wenn sie dort keinen Sinn ergeben.
IFileReader und IFileWriter beschreiben genau die Bedürfnisse des Konsumenten.
public interface IFileReader
{
Task<byte[]> Read(string key, CancellationToken ct);
}
public interface IFileWriter
{
Task Save(string key, byte[] data, CancellationToken ct);
}
public sealed class ArchiveStore : IFileReader { /* ... */ }
public sealed class BlobStore : IFileReader, IFileWriter { /* ... */ }
Wann ist Vererbung sinnvoll?
- Die „ist ein“-Beziehung stimmt fachlich, nicht nur im Code.
- Der Basisvertrag ist klein, stabil und durch Vertragstests geschützt.
- Die Unterklasse schwächt Ergebnisse nicht ab und verlangt keine zusätzlichen Vorbedingungen.
- Die Basisklasse gibt nicht viel geschützten veränderlichen Zustand frei.
- Alle Implementierungen bewahren Fehler-, Abbruch- und Nullwertsemantik.
Guter Fall: ein stabiles Prozessgerüst
Template Method passt, wenn die Schrittfolge zum stabilen Vertrag gehört und der erweiterbare Teil klein und präzise definiert ist. Gemeinsame Tests für alle Unterklassen bleiben nötig. Umgehen Unterklassen Schritte oder ändern ihre Reihenfolge, wird Komposition verständlicher.
Entscheidung 2 von 4
Eine Implementierung wirft „Vorgang nicht unterstützt“. Was bedeutet das meist?
Es reicht, die Exception zu dokumentieren
Der Vertrag ist zu breit
Wir brauchen noch eine Basisklasse
Prüfe, ob die Beziehung das Liskov-Prinzip erfüllt.
3. Polymorphie: eine Entscheidung, mehrere korrekte Ausführungen
Polymorphie hilft, wenn Aufrufer den Vertrag, aber keine konkreten Typen kennen müssen. Daneben sollte kein zweiter Auswahlmechanismus in einem switchversteckt sein.
public interface IReleaseCheck
{
string Code { get; }
Task<CheckResult> Evaluate(ReleaseContext context, CancellationToken ct);
}
public sealed class SecretSourceCheck : IReleaseCheck
{
public string Code => "secret_source";
public async Task<CheckResult> Evaluate(
ReleaseContext context,
CancellationToken ct)
{
var bindings = await context.Secrets.GetBindings(ct);
var invalid = bindings
.Where(x => x.Environment == "production" && !x.FromVault);
return invalid.Any()
? CheckResult.Block(Code, "Produktionsgeheimnisse müssen aus dem Vault stammen.")
: CheckResult.Pass(Code);
}
}
public sealed class ReleaseGate
{
private readonly IReadOnlyList<IReleaseCheck> checks;
public ReleaseGate(IEnumerable<IReleaseCheck> checks)
=> this.checks = checks.ToList();
public async Task<ReleaseDecision> Evaluate(
ReleaseContext context,
CancellationToken ct)
{
var results = new List<CheckResult>();
foreach (var check in checks)
results.Add(await check.Evaluate(context, ct));
return ReleaseDecision.From(results);
}
}
ReleaseGate kennt weder Secret- noch Migrations- oder API-Vertragsprüfung. Es kennt einen Vertrag und sammelt Ergebnisse. Neues Verhalten ist eine neue Implementierung statt einer weiteren Verzweigung im stabilen Kern.
4. Komposition statt tiefer Vererbungsbäume
Im Vererbungsbaum wirkt eine Basisänderung vertikal auf Unterklassen. Bei Komposition erhält ein Objekt kleine Zusammenarbeitspartner und zeigt ausdrücklich, was es braucht.
Tiefer Baum
Ausdrückliche Komposition
Komposition bedeutet kein Interface für jede Klasse. Grenzen gehören an echte Variabilität, Nebenwirkungen oder kontrolliert austauschbare Testabhängigkeiten.
5. SOLID ohne Glaubenskrieg
Wie viele unabhängige Änderungsgründe hat das Modul?
Verlangt ein neuer Fall Änderungen im stabilen Kern?
Hält jede Implementierung das Vertragsversprechen?
Hängt der Client nur von benötigten Fähigkeiten ab?
Hängt die Regel von einem Begriff oder einem konkreten SDK ab?
Wenn ReleaseService Policies prüft, Azure aufruft, Audits speichert, E-Mails formatiert und HTML erzeugt, bestehen viele Änderungsgründe. Die Trennung verkleinert den Bereich, der für die nächste Funktion verstanden und getestet werden muss.
6. Ein neues Modul von Grund auf bauen
„Das System blockiert einen Release, wenn ein Produktionssecret nicht aus freigegebener Quelle stammt.“ Beginne nicht beim Klassennamen.
Erfolg, Blockade, fehlende Daten, Timeout, Abbruch. Beispiele legen den Vertrag offen.
Eine reine Regel und ein eigener Anbieteradapter ergeben schnelle Tests und klare Fehler.
Übergib Entscheidungsdaten statt des gesamten Cloud-Clients oder Servicecontainers.
Regel, Eingang, Audit, Metrik und Test. Erst eine zweite Implementierung bestätigt die gemeinsame Abstraktion.
Test, Korrelationslog, Metrik, Operatormeldung und Rückweg.
Entscheidung 3 von 4
Du hast nur eine Implementierung und keine bestätigte Variabilität. Was tust du?
Ich baue sofort ein Pluginsystem
Ich beginne einfach und lasse eine klare Nahtstelle
Ich verbinde die Regel direkt mit dem SDK
Wähle die Abstraktion passend zu deinem heutigen Wissen.
7. Sauberer Code bedeutet nicht kurzer Code
Zwölf Zeilen können drei Verantwortungen vermischen; dreißig Zeilen können einen zusammenhängenden Algorithmus verständlich erklären. Beurteile Entscheidungsfluss statt Zeilenanzahl.
EvaluateRelease sagt mehr als ProcessDataversteckt sein.
Weniger versteckter Zustand bedeutet weniger Bedingungen im Kopf.
Eine negative fachliche Entscheidung sollte ein ausdrückliches Ergebnis sein.
Name und Abhängigkeiten zeigen Netzwerk- oder Datenbankzugriffe.
Kopiere dieselbe Validierung nicht in Controller, Job und Frontend.
Logge keine vollständigen Objekte mit Secrets oder privaten Daten.
Der beste Name beschreibt keine Sprachkonstruktion, sondern die Entscheidung des Codes.
8. Fremden Code übernehmen
Beginne nicht mit Formatierung und Ordnerumzügen. Erstelle zuerst eine Verhaltenskarte. Geduld ist hier wertvoller als Schreibtempo.
Versionen, Konfiguration, Migrationen und minimalen Smoke-Test festhalten.
Eingänge, Ausgänge, Daten, Integrationen, Queues und Zeitpläne.
Was darf niemals passieren?
Charakterisierungstests für das aktuelle Verhalten ergänzen.
Adapter, Handler, Interface oder Feature Flag.
Kleiner Commit, kontrollierter Release und Beobachtung.
[Fact]
public async Task Legacy_gate_blocks_when_any_required_check_fails()
{
var checks = new IReleaseCheck[]
{
StubCheck.Pass("contract"),
StubCheck.Block("secrets", "missing vault binding")
};
var decision = await new ReleaseGate(checks)
.Evaluate(Context.Valid(), CancellationToken.None);
decision.Allowed.Should().BeFalse();
decision.Blockers.Should().ContainSingle(x => x.Code == "secrets");
}
Ein Charakterisierungstest erklärt alten Code nicht für perfekt. Er bestätigt, dass wir sein aktuelles Verhalten verstehen – ein Sicherheitsgurt beim Strukturieren.
Entscheidung 4 von 4
Refactoring und neue Funktion berühren dieselben Dateien. Wie senkst du das Risiko?
Ich kombiniere alles in einem großen Pull Request
Ich trenne Absicherung, Refactoring und Funktion
Ich ändere erst Code und schreibe Tests später
Wähle eine Strategie, die Review und Rücknahme erleichtert.
9. Warnsignale in übernommenen Systemen
| Signal | Mögliche Bedeutung | Erster sicherer Schritt |
|---|---|---|
| Tiefer Vererbungsbaum | Versteckter Zustand und fragile Abhängigkeiten | Gemeinsamer Vertragstest der Unterklassen |
switch nach Typ | Scheinpolymorphie | Implementierungsregister hinter einem Vertrag |
NotSupportedException | LSP-Verletzung | Vertrag nach Fähigkeiten teilen |
| Service mit vielen SDKs | Viele Änderungsgründe | Entscheidungen von Adaptern trennen |
| Viele Mocks, keine Integrationstests | Unbekannter Systemvertrag | Charakterisierungstest an der Grenze |
Utils und Common | Unklare Zuständigkeit | Operation dem passenden Begriff zuordnen |
| Boolean steuert Methode | Zwei Vorgänge unter einem Namen | Separate Commands oder Strategien |
| Vollständige Objekte loggen | Risiko für personenbezogene Daten und Secrets | Ausdrückliches Telemetriedatenmodell |
10. Dieselben Entscheidungen in verschiedenen Sprachen
Syntax verändert sich, Verantwortungsgrenzen und Verträge bleiben wichtig.
| Absicht | C# | Java/Kotlin | TypeScript | Python |
|---|---|---|---|---|
| Kleiner Vertrag | interface | interface | Interface oder struktureller Typ | Protocol / ABC |
| Geschlossene Ergebnismenge | record + Enum | Sealed Class | Discriminated Union | Dataclass + Enum |
| Verhalten zusammensetzen | Konstruktor | Konstruktor | Funktionen oder Objekte | ausdrückliche Abhängigkeiten |
| Vertragstest | gemeinsame Fixture | Contract Test | gemeinsamer Testsatz | parametrisierte Tests |
| Infrastrukturadapter | Port-Implementierung | Port-Implementierung | Objekt, das den Typ erfüllt | Objekt, das Protocol erfüllt |
Die universellen Fragen bleiben: Wo liegt die Entscheidung, wo die Nebenwirkung, wie lautet der Vertrag, und wie beweisen wir einen sicheren Austausch des Verhaltens?
11. Polymorphie testen
Jede Implementierung liefert ein Ergebnis mit eigenem Code.
Jede respektiert den Abbruch.
Keine liefert null statt eines Ergebnisses.
Keine verändert den Eingabekontext.
Fehlende Daten haben einen gemeinsamen ausdrücklichen Status.
Meldungen enthalten keine Secrets oder personenbezogenen Daten.
Anbieterfehler unterscheiden sich von fachlichen Blockaden.
Der gemeinsame Vertragstest funktioniert für jeden Typ.
12. Architektur nicht übertreiben
Erzeuge kein Interface nur, weil eine Klasse existiert. Keine Factory, wenn Konstruktor und DI die Auswahl lösen. Kein Pluginsystem für zwei stabile Fälle. Teile Methoden nicht nur, damit jede fünf Zeilen hat.
Abstraktion lohnt sich, wenn sie reale Änderungskosten senkt oder echte Nebenwirkungen isoliert. Gute Architektur ist etwas langweilig: Abhängigkeiten sind sichtbar, Abläufe nachvollziehbar, und neue Teammitglieder erkennen die nächste Änderungsstelle.
Interaktiver Ablauf
Führe eine Änderung durch fünf Sicherheitsprüfungen
Starte die Animation und sieh die Reihenfolge, die Risiken beim Übernehmen unbekannten Codes begrenzt.
Ablauf starten
Bereit zum Start.
Checkliste vor der Änderung
- Ich kann das Verhalten in einem Satz beschreiben.
- Ich kenne die unantastbare Invariante.
- Ich kenne den Eingang, der diesen Code startet.
- Ich sehe alle Nebenwirkungen.
- Ich habe einen Test des bisherigen Verhaltens.
- Ich habe Komposition oder Vererbung bewusst gewählt.
- Jede Implementierung erfüllt denselben Vertrag.
- Eine neue Abstraktion entspricht echter Variabilität.
- Logs und Metriken prüfen Produktion.
- Rollback erfordert keine Rücknahme destruktiver Migrationen.
Der wichtigste Gedanke
Guter Code ist nicht unzerstörbar. Er macht Änderungsrisiken früh sichtbar. Vererbung kann stabile Verträge modellieren, Polymorphie zentrale Auswahlzweige entfernen, SOLID Kopplungen benennen. Nichts davon ersetzt Verhaltensverständnis, Tests und Beobachtung nach dem Deployment.
Beginnst du neu, starte bei Ergebnis und Grenzen. Übernimmst du fremden Code, beobachte zuerst, sichere Verhalten, finde eine Nahtstelle und ändere erst danach die Struktur.
Für dein Projekt
Fünf Fragen, wichtiger als die Programmiersprache
Welche fachliche Entscheidung schützt dieser Code?
Sind Typen ohne Vertragsausnahmen austauschbar?
Wo endet die Regel, wo beginnt Infrastruktur?
Welcher Test schützt Verhalten beim Refactoring?
Welches Signal bestätigt die Änderung nach dem Deployment?
Ist dein Code schwer sicher zu ändern?
Beginne mit einer Karte von Verhalten und Risiken, nicht mit einem Rewrite.
Eine kurze Beschreibung von Modul, Fehler oder geplanter Änderung reicht, um den ersten technischen Schritt zu klären.
Technisches Problem beschreiben