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.

01VerstehenVerhalten und Risiko
02AbsichernCharakterisierungstest
03TrennenVertrag und Nebenwirkungen
04Ändernkleiner vertikaler Schritt
05BestätigenMetrik und Rollback

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.

VertragWas verspricht das Modul heute?

Was Konsumenten tatsächlich voraussetzen, nicht nur was einmal geplant war.

NebenwirkungenWo berührt das System die Außenwelt?

Datenbank, Netzwerk, Dateien, Queue, Cloud, E-Mail und Telemetrie.

NachweisWas schützen die Tests?

Fachliches Verhalten oder nur die Reihenfolge von Mock-Aufrufen?

Kleinster SchrittWo entsteht eine sichere Nahtstelle?

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.

VorherEin breiter Vertrag

Lesen, Schreiben und Löschen gelten für jede Unterklasse, auch wenn sie dort keinen Sinn ergeben.

→
NachherFähigkeiten statt Etiketten

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.

EingangReleaseContextUmgebung, Artefakt, Abhängigkeiten
01Secrets
02Database
03Contract
ErgebnisReleaseDecisionpass, warn oder block

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

BaseReleaseCheck
CloudReleaseCheck
AzureReleaseCheck
ProductionAzureReleaseCheck

Ausdrückliche Komposition

ReleaseGate
IReleaseCheck[]IClockIAuditWriterIReleasePolicy

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

SSingle Responsibility

Wie viele unabhängige Änderungsgründe hat das Modul?

OOpen/Closed

Verlangt ein neuer Fall Änderungen im stabilen Kern?

LLiskov Substitution

Hält jede Implementierung das Vertragsversprechen?

IInterface Segregation

Hängt der Client nur von benötigten Fähigkeiten ab?

DDependency Inversion

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

01
Ergebnis benennen

„Das System blockiert einen Release, wenn ein Produktionssecret nicht aus freigegebener Quelle stammt.“ Beginne nicht beim Klassennamen.

02
Beispiele notieren

Erfolg, Blockade, fehlende Daten, Timeout, Abbruch. Beispiele legen den Vertrag offen.

03
Entscheidung von I/O trennen

Eine reine Regel und ein eigener Anbieteradapter ergeben schnelle Tests und klare Fehler.

04
Den engsten Vertrag wählen

Übergib Entscheidungsdaten statt des gesamten Cloud-Clients oder Servicecontainers.

05
Einen vertikalen Ausschnitt bauen

Regel, Eingang, Audit, Metrik und Test. Erst eine zweite Implementierung bestätigt die gemeinsame Abstraktion.

06
Abnahmenachweis benennen

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.

Namen zeigen Absicht

EvaluateRelease sagt mehr als ProcessDataversteckt sein.

Daten fließen in eine Richtung

Weniger versteckter Zustand bedeutet weniger Bedingungen im Kopf.

Exceptions bleiben Ausnahmen

Eine negative fachliche Entscheidung sollte ein ausdrückliches Ergebnis sein.

Nebenwirkungen sind sichtbar

Name und Abhängigkeiten zeigen Netzwerk- oder Datenbankzugriffe.

Eine Regel hat einen Verantwortlichen

Kopiere dieselbe Validierung nicht in Controller, Job und Frontend.

Telemetrie hat ein eigenes Modell

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.

1Starten

Versionen, Konfiguration, Migrationen und minimalen Smoke-Test festhalten.

2Kartieren

Eingänge, Ausgänge, Daten, Integrationen, Queues und Zeitpläne.

3Invarianten benennen

Was darf niemals passieren?

4Absichern

Charakterisierungstests für das aktuelle Verhalten ergänzen.

5Nahtstelle finden

Adapter, Handler, Interface oder Feature Flag.

6Ändern und messen

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

SignalMögliche BedeutungErster sicherer Schritt
Tiefer VererbungsbaumVersteckter Zustand und fragile AbhängigkeitenGemeinsamer Vertragstest der Unterklassen
switch nach TypScheinpolymorphieImplementierungsregister hinter einem Vertrag
NotSupportedExceptionLSP-VerletzungVertrag nach Fähigkeiten teilen
Service mit vielen SDKsViele ÄnderungsgründeEntscheidungen von Adaptern trennen
Viele Mocks, keine IntegrationstestsUnbekannter SystemvertragCharakterisierungstest an der Grenze
Utils und CommonUnklare ZuständigkeitOperation dem passenden Begriff zuordnen
Boolean steuert MethodeZwei Vorgänge unter einem NamenSeparate Commands oder Strategien
Vollständige Objekte loggenRisiko für personenbezogene Daten und SecretsAusdrückliches Telemetriedatenmodell

10. Dieselben Entscheidungen in verschiedenen Sprachen

Syntax verändert sich, Verantwortungsgrenzen und Verträge bleiben wichtig.

AbsichtC#Java/KotlinTypeScriptPython
Kleiner VertraginterfaceinterfaceInterface oder struktureller TypProtocol / ABC
Geschlossene Ergebnismengerecord + EnumSealed ClassDiscriminated UnionDataclass + Enum
Verhalten zusammensetzenKonstruktorKonstruktorFunktionen oder Objekteausdrückliche Abhängigkeiten
Vertragstestgemeinsame FixtureContract Testgemeinsamer Testsatzparametrisierte Tests
InfrastrukturadapterPort-ImplementierungPort-ImplementierungObjekt, das den Typ erfülltObjekt, 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

1BeobachtenEingänge und Nebenwirkungen
2AbsichernTest des aktuellen Verhaltens
3Nahtstelle findenVertrag und Adapter
4Austauschenkleiner vertikaler Schritt
5BestätigenMetrik und Rollback

Bereit zum Start.

Checkliste vor der Änderung

  1. Ich kann das Verhalten in einem Satz beschreiben.
  2. Ich kenne die unantastbare Invariante.
  3. Ich kenne den Eingang, der diesen Code startet.
  4. Ich sehe alle Nebenwirkungen.
  5. Ich habe einen Test des bisherigen Verhaltens.
  6. Ich habe Komposition oder Vererbung bewusst gewählt.
  7. Jede Implementierung erfüllt denselben Vertrag.
  8. Eine neue Abstraktion entspricht echter Variabilität.
  9. Logs und Metriken prüfen Produktion.
  10. 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

01

Welche fachliche Entscheidung schützt dieser Code?

02

Sind Typen ohne Vertragsausnahmen austauschbar?

03

Wo endet die Regel, wo beginnt Infrastruktur?

04

Welcher Test schützt Verhalten beim Refactoring?

05

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