Das Decorator Pattern

Das Decorator Pattern gehört zu den flexibelsten Entwurfsmustern. In Kombination mit anderen Patterns und Tools ermöglicht es, bestehende Funktionalität zu erweitern, ohne vorhandenen Code zu verändern – ein zentraler Vorteil in wartbaren Anwendungen.

Nach dem Prinzip „Composition over Inheritance“ ist es oft besser, Funktionalität durch Aggregation bereitzustellen statt durch Vererbung. Vererbung bringt zwei zentrale Nachteile mit sich:

  • Sie erzeugt eine enge Kopplung zwischen Basisklasse und abgeleiteter Klasse, was Änderungen erschwert.
  • Die abgeleitete Klasse ist oft keine echte Spezialisierung der Basisklasse, sondern lediglich ähnlich im Verhalten.

Ein klassisches Beispiel ist das Rechteck-Quadrat-Dilemma. In der Mathematik ist ein Quadrat ein Spezialfall eines Rechtecks – in der Softwareentwicklung funktioniert diese Vererbung jedoch nicht ohne Seiteneffekte:

public class Rectangle
{
    public double X { get; protected set; }
    public double Y { get; protected set; }
    public virtual void SetX(double x) => X = x;
    public virtual void SetY(double y) => Y = y;
}

public class Square : Rectangle
{
    public override void SetY(double y) => X = Y = y;
    public override void SetX(double x) => X = Y = x;
}

Das ändert das Verhalten von Rectangle in unvorhersehbarer Weise:

Rectangle r = new Rectangle();
r.SetX(5);
r.SetY(7);
Console.WriteLine(r.X); // 5

Rectangle r2 = new Square();
r2.SetX(5);
r2.SetY(7);
Console.WriteLine(r2.X); // 7 – nicht wie erwartet!

In übersichtlichem Code ist dieses Verhalten vielleicht noch nachvollziehbar – in komplexen Anwendungen mit getrennten Initialisierungen nicht mehr. Die Ursache: Ein Quadrat ist kein Rechteck, sondern hat lediglich ähnliche Eigenschaften. Eine Konvertierung wäre hier sinnvoller als Vererbung.

Decorator Pattern: Erweiterung ohne Seiteneffekte

Genauso wenig wie X sich beim Ändern von Y verändern sollte, sollte eine Komponente nicht plötzlich Logging oder Bindings enthalten, nur weil sie in einem neuen Kontext genutzt wird. Diese Features gehören nicht zur Kernverantwortung der Klasse.

Das Decorator Pattern erweitert eine bestehende Funktionalität, ohne deren Interface zu verändern. Es benötigt daher ein gemeinsames Interface – und idealerweise ist die zu dekorierende Klasse über dieses Interface bereits abstrahiert:

public interface IUserManager
{
    void AddUser(User user);
}

public class UserManager : IUserManager
{
    public void AddUser(User user)
    {
        // Originale Logik
    }
}

public class LoggingDecorator : IUserManager
{
    private readonly IUserManager _inner;
    private readonly ILogger<LoggingDecorator> _logger;

    public LoggingDecorator(IUserManager inner, ILogger<LoggingDecorator> logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public void AddUser(User user)
    {
        _logger.LogInformation("User wird hinzugefügt");
        _inner.AddUser(user);
        _logger.LogInformation($"User hinzugefügt mit ID {user.Id}");
    }
}

public class ValidationDecorator : IUserManager
{
    private readonly IUserManager _inner;

    public ValidationDecorator(IUserManager inner)
    {
        _inner = inner;
    }

    public void AddUser(User user)
    {
        if (string.IsNullOrWhiteSpace(user.Name))
            throw new ArgumentException("Name ist erforderlich");
        _inner.AddUser(user);
    }
}

Die Decorators lassen sich flexibel kombinieren:

IUserManager baseManager = new UserManager();
IUserManager validated = new ValidationDecorator(baseManager);
IUserManager withLogging = new LoggingDecorator(validated, logger);

Integration mit Dependency Injection

Auch in Kombination mit Microsoft.Extensions.DependencyInjection ist das Pattern einsetzbar. Beispiel:

builder.Services.AddTransient();
builder.Services.AddTransient(sp =>
{
    var baseService = sp.GetRequiredService();
    var validated = new ValidationDecorator(baseService);
    var logger = sp.GetRequiredService<ILogger<LoggingDecorator>>();
    return new LoggingDecorator(validated, logger);
});

Diese Technik erlaubt vollständige Kontrolle über Aufbau und Reihenfolge der Decorators zur Laufzeit – ohne magische Auto-Erweiterungen oder versteckte Registrierungen.

  • Neue Features lassen sich als weitere Decorators ergänzen.
  • Jeder Aspekt bleibt einzeln testbar.
  • Es entsteht keine unnötige Verzweigung oder Abhängigkeit innerhalb der Kernklasse.
  • Das Pattern hilft, Verstöße gegen das Single Responsibility Principle sichtbar zu machen – z. B. wenn innerhalb einer Methode geloggt werden soll.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert