Ein Decorator hat genau zwei Eigenschaften. Beide sind notwendig, und zusammen genügen sie:
- Er implementiert die Schnittstelle, die er dekoriert.
- Er enthält eine Instanz derselben Schnittstelle und delegiert an sie.
Das ist alles. Die erste Eigenschaft macht ihn für den Aufrufer unsichtbar, die zweite macht ihn stapelbar.
public interface IPreisRepository
{
Task<decimal> HolePreisAsync(string artikelNr);
}
// Der Kern: nur noch die Abfrage.
public sealed class PreisRepository : IPreisRepository
{
private readonly IDbConnection _db;
public PreisRepository(IDbConnection db) => _db = db;
public Task<decimal> HolePreisAsync(string artikelNr) =>
_db.QuerySingleAsync<decimal>(
"select preis from artikel where nr = @nr", new { nr = artikelNr });
}Und jedes Anliegen für sich:
public sealed class GecachtesPreisRepository : IPreisRepository
{
private readonly IPreisRepository _innen; // dieselbe Schnittstelle
private readonly IMemoryCache _cache;
public GecachtesPreisRepository(IPreisRepository innen, IMemoryCache cache)
{
_innen = innen;
_cache = cache;
}
public async Task<decimal> HolePreisAsync(string artikelNr)
{
if (_cache.TryGetValue(artikelNr, out decimal gecacht))
return gecacht; // gar nicht erst nach innen
var preis = await _innen.HolePreisAsync(artikelNr);
_cache.Set(artikelNr, preis, TimeSpan.FromMinutes(5));
return preis;
}
}
public sealed class ProtokolliertesPreisRepository : IPreisRepository
{
private readonly IPreisRepository _innen;
private readonly ILogger<ProtokolliertesPreisRepository> _logger;
public ProtokolliertesPreisRepository(
IPreisRepository innen, ILogger<ProtokolliertesPreisRepository> logger)
{
_innen = innen;
_logger = logger;
}
public async Task<decimal> HolePreisAsync(string artikelNr)
{
var uhr = Stopwatch.StartNew();
var preis = await _innen.HolePreisAsync(artikelNr);
_logger.LogInformation(
"Preis {Artikel} in {Ms}ms", artikelNr, uhr.ElapsedMilliseconds);
return preis;
}
}Zusammengesteckt wird von innen nach aussen:
IPreisRepository repo =
new ProtokolliertesPreisRepository(
new GecachtesPreisRepository(
new PreisRepository(verbindung),
cache),
logger);Der Aufrufer merkt nichts
Wer IPreisRepository benutzt, sieht in beiden Fällen dieselbe Schnittstelle mit derselben Methode. Ob dahinter eine Klasse steht oder vier, ist von aussen weder sichtbar noch relevant — und genau deshalb kannst du Schichten hinzufügen und entfernen, ohne einen einzigen Aufrufer anzufassen.
Die Reihenfolge ist eine Entscheidung
Beim Vererbungsansatz war die Reihenfolge festgelegt. Hier wählst du sie — und die Wahl hat Folgen:
Protokoll aussen oder innen?
Protokoll aussen um Cache
new Protokolliert(
new Gecacht(
new Repository(db)))
// Protokolliert JEDE Anfrage,
// auch die aus dem Cache.
// Millisekunden = was der
// Aufrufer wirklich wartet.Cache aussen um Protokoll
new Gecacht(
new Protokolliert(
new Repository(db)))
// Protokolliert nur die Anfragen,
// die die Datenbank erreichen.
// Cache-Treffer bleiben unsichtbar.Beides ist verteidigbar — links misst du die erlebte Antwortzeit, rechts die Datenbanklast. Falsch ist nur, die Reihenfolge nicht bewusst zu wählen.
Kurz nachgedacht
Zählt nicht — nur zum Prüfen, ob es angekommen ist.
1.Was macht eine Klasse zum Decorator?