Das Decorator Pattern ist eins meiner Lieblingspattern, weil es kombiniert mit anderen Pattern und Tools eine Anwendung sehr flexibel macht ohne bestehenden Code für ein neues Feature zu verändern.
Nach dem Motto ‚Aggregation over inheritance‘ ist es besser eine Instanz einer Klasse zu nutzen als von dieser zu erben. Die Vererbung birgt immer zwei Probleme:
- Es erzeugt eine starke Abhängigkeit zwischen beiden Klassen, die eine Trennung voneinander nicht möglich macht.
- Die erbende Klasse ist per Definition keine echte Unterklasse, sondern ist nur sehr ähnlich zu dem Verhalten der vererbten Klasse.
Echte Vererbung ist seltener als oft vermutet. Als Beispiel wird gerne das Problem bei den mathematischen Formen Rechteck und Quadrat gewählt. In der Mathematik ist ein Rechteck ein Quadrat, wenn beide Seiten gleich lang sind. Die Vermutung liegt also nahe die Klassen so aufzubauen:
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; }
Nur verändert dies das Verhalten der vererbten Klasse Rectangle.
Rectangle r = new Rectangle(); r.SetX(5); r.SetY(7); Console.WriteLine(r.X); //5 Rectangle r2 = new Square(); r.SetX(5); r.SetY(7); Console.WriteLine(r.X); //7
Untereinander geschrieben mag das Verhalten noch nachvollziehbar sein, aber spätestens wenn die Initialisierung in unterschiedlichen Bereichen der Anwendung stattfinden, ist das Verhalten unerwünscht und nicht nachvollziehbar.
Der Grund ist, dass ein Rechteck kein Quadrat ist, sondern die Eigenschaften eines Quadrates aufweist. Besser wäre es also beide Typen nicht voneinander erben zu lassen, sondern eine Konvertierung anzubieten, wenn ein Rechteck die Eigenschaften eines Quadrates aufweist. In die andere Richtung ist die Konvertierung immer möglich.
Nach dem kleinen Ausflug, zurück zum Decorator Pattern: Genauso wenig, wie ich erwarte, dass sich X bei meinem Rechteck verändert, wenn ich Y verändere, erwarte ich auch nicht, dass eine Datenstruktur Binding unterstützt oder irgendeine Management-Klasse einen Logger benötigt um zu funktionieren. Beides sind Features, die aber nicht zu der Aufgabe der Klasse gehört und das Verhalten der Klasse ändert.
Um Features für einen speziellen Typen zur Verfügung zu stellen, nutzt man das Decorator Pattern. Als Konstruktorparameter nimmt es die ursprüngliche Klasse und bietet nach außen dieselben Aufrufe. Daher wird hierfür auch immer ein Interface benötigt:
public interface IUserManager { void AddUser(User user); } public class UserManager : IUserManager { public void AddUser(User user) { // ... } } public class UserManagerLoggingDecorator : IUserManager { IUserManager _userManager; ILogger<IUserManagerLoggingDecorator> _logger; public UserManagerLoggingDecorator(IUserManager userManager, ILogger<UserManagerLoggingDecorator> logger) { _userManager = userManager; _logger = logger; } public void AddUser(User user) { _logger.LogInformation("User will be added"); // ... _logger.LogInformation($"User added with id {user.Id}"); } }
Die Klasse kann nun mit und ohne Logging verwendet werden, sie bleibt sehr einfach zu testen und hat keine weiteren Abhängigkeiten außer die für ihre Aufgabe notwendigen.
IUserManager userManager = new UserManager(); //Oder IUserManager userManager = new UserManagerLoggingDecorator(new UserManager());
- Wird im späteren Verlauf der Anwendung ein neuer UserManager implementiert, ist das Logging bereits fertig.
- Wird ein weiteres Feature benötigt, implementiert man es auch als Decorator und kann jedes der Features selbst während der Runtime einfach ein oder ausschalten.
- Und mein Lieblingspunkt: Fragst du dich grade, wie du mit einem Decorator innerhalb einer Methode loggen sollst? Gar nicht. Besteht das Bedürfnis innerhalb einer Methode loggen zu müssen, dann benötigt der Code ein Refactoring, denn es scheint als würde etwas sehr wichtiges innerhalb der Methode zusammen mit anderen Operationen ausgeführt, dass entweder einer eigenen Methode bedarf oder sogar eine eigene Klasse. Das Pattern hilft bereits Einsteigern selbst zu erkennen wann eine Methode mehr macht als sie sollte.