Ein Repository holt Preise aus der Datenbank. Es ist eine kurze, klare Klasse — bis die Anforderungen kommen, die mit Preisen nichts zu tun haben.
«Die Abfrage ist zu langsam, cache das.» — «Wir sehen nicht, welche Abfragen laufen, log das.» — «Die Datenbank hat manchmal Aussetzer, versuch es zweimal.»
Alle drei landen im selben Methodenrumpf:
public async Task<decimal> HolePreisAsync(string artikelNr)
{
if (_cache.TryGetValue(artikelNr, out decimal gecacht))
return gecacht;
_logger.LogInformation("Preisabfrage {Artikel}", artikelNr);
var uhr = Stopwatch.StartNew();
for (var versuch = 1; ; versuch++)
{
try
{
var preis = await _db.QuerySingleAsync<decimal>(
"select preis from artikel where nr = @nr", new { nr = artikelNr });
_cache.Set(artikelNr, preis, TimeSpan.FromMinutes(5));
_logger.LogInformation("Preisabfrage {Artikel} in {Ms}ms", artikelNr, uhr.ElapsedMilliseconds);
return preis;
}
catch (SqlException) when (versuch < 3)
{
await Task.Delay(100 * versuch);
}
}
}Die eine Zeile, um die es geht, steht in der Mitte. Alles andere ist Beiwerk — und dieses Beiwerk steht in jedem Repository noch einmal, leicht anders geschrieben, weil es jemand anders getippt hat.
Zwei Dinge sind hier vermischt:
- Was die Klasse tut: einen Preis holen.
- Wie sie betrieben wird: gecacht, protokolliert, mit Wiederholung.
Das Zweite ist in jedem Repository dasselbe und ändert sich unabhängig vom Ersten. Ein Cache-Ablauf von fünf auf zehn Minuten ist eine Betriebsentscheidung; sie sollte nicht bedeuten, dass eine Datenzugriffsklasse angefasst wird.
Der naheliegende Ausweg führt in die Irre
Der erste Reflex ist Vererbung: eine Basisklasse GecachtesRepository, davon erben alle. Das trägt genau bis zur zweiten Anforderung.
class PreisRepository { }
class GecachtesPreisRepository : PreisRepository { }
class ProtokolliertesPreisRepository : PreisRepository { }
class GecachtesProtokolliertesPreisRepository : ??? { }
// und mit Wiederholung? und in der anderen Reihenfolge?Warum das nicht aufgeht
Bei drei Anliegen brauchst du für jede Kombination eine Klasse — und C# erlaubt nur eine Basisklasse, also lässt sich Gecacht und Protokolliert nicht einmal kombinieren, ohne die eine in die andere zu schreiben. Die Anzahl der Klassen wächst mit den Kombinationen, nicht mit den Anliegen.
Die Umkehrung
Decorator dreht die Beziehung um: statt zu erben, wird umschlossen.
Jedes Anliegen wird eine eigene Klasse, die dieselbe Schnittstelle erfüllt und eine weitere Instanz derselben Schnittstelle in sich trägt. Sie tut ihre eine Sache und gibt die Arbeit nach innen weiter.
Aus «erben von» wird «enthält ein». Drei Anliegen bleiben drei Klassen — in jeder Reihenfolge und jeder Kombination zusammensteckbar.