Hallo,
bin neu hier im Forum, also erstmal Hallo an alle, die das lesen! 😃
So, und gleich zu meinem Problem! Ich programmiere zwar schon ne Weile c# fange aber jetzt erst an, mich mal etwas intensiver mit den Design Patterns zu beschäftigen. Dazu hab ich ein Buch ausm Regal gezogen, das ich schon viele Jahre hier stehen habe, nämlich das Entwurfsmuster-Buch aus der Kopf bis Fuß-Reihe.
Ich versuche gerade das Beispiel des Decorator Patterns (im Buch sind die Bsp zwar in Java abgedruckt, aber das Prinzip ist ja trotzdem nicht so schwer zu verstehen) nachzubauen und bin da auf ein Problem gestoßen, das mich gerade etwas überfordert - und ich hoffe, dass ihr es mir nachseht, wenn das eine komplett banale (Grundlagen-)Frage sein sollte.
Also, wie im Buch auch, hab ich folgende Klassen gemacht. Erstmal die Klasse, von der alles ausgeht, inkl. zweier direkter Subklassen:
public abstract class Getraenk
{
internal string _beschreibung = "Unbekanntes Getränk";
public string Beschreibung
{
get { return _beschreibung; }
set
{
_beschreibung = value;
}
}
public string GetBeschreibung()
{
return Beschreibung;
}
public abstract float GetPreis();
}
public class Espresso : Getraenk
{
public Espresso()
{
Beschreibung = "Espresso";
}
public override float GetPreis()
{
return 1.99f;
}
}
Dann die Decorator-Klasse (die ja auch von der "Ausgangsklasse" erbt), auch mit zwei Subklassen:
public abstract class ZutatenDekorierer : Getraenk
{
public abstract new string GetBeschreibung();
}
public class Schoko : ZutatenDekorierer
{
Getraenk _getraenk;
public Schoko(Getraenk getraenk)
{
_getraenk = getraenk;
}
public override string GetBeschreibung()
{
return _getraenk.GetBeschreibung() + ", Schoko";
}
public override float GetPreis()
{
return _getraenk.GetPreis() + 0.20f;
}
}
public class HeisseMilch : ZutatenDekorierer
{
Getraenk _getraenk;
public HeisseMilch(Getraenk getraenk)
{
_getraenk = getraenk;
}
public override string GetBeschreibung()
{
return _getraenk.GetBeschreibung() + ", Heiße Milch";
}
public override float GetPreis()
{
return _getraenk.GetPreis() + 0.10f;
}
}
Und schließlich das Zusammenbauen in der main-methode:
Getraenk getraenk1 = new Espresso();
getraenk1 = new Schoko(getraenk1);
getraenk1 = new HeisseMilch(getraenk1);
System.Console.WriteLine("{0,-50}{1:0.##}€", getraenk1.GetBeschreibung(), getraenk1.GetPreis());
Nun hätte ich erwartet, dass als Beschreibung sowas wie "Espresso, Schoko, heiße Milch" ausgegeben würde - stattdessen aber krieg ich "Unbekanntes Getränk". Der Preis wird richtig berechnet.
Wenn ich rein debugge seh ich natürlich, dass die GetBeschreibung der Klasse Espresso verwendet wird. Aber warum ist das so? Final ist getraenk1 ja eine Instanz von HeisseMilch - warum wird dann nicht deren GetBeschreibung-Methode verwendet? Sorry, steh echt aufm Schlauch!
Würde mir da bitte jemand auf die Sprünge helfen?
Ich danke herzlichst! 😃
Johann
Weil die Implementierung nicht ganz so korrekt ist.
Deine Grundimplementierung muss abstract sein; dann kannst den Rest overriden und es funktioniert wie gedacht. Zudem führt Dein new bei der Vererbung der Methoden dazu, dass es im Gegensazu zu override eben nicht überschrieben, sondern nur versteckt wird. Dadurch verwendet das Boxing (also der darunterliegende Typ) seine Implementierung, und nicht die Deines abgeleiteten Typs (Knowing When to Use Override and New Keywords (C# Programming Guide))
The override modifier may be used on virtual methods and must be used on abstract methods. This indicates for the compiler to use the last defined implementation of a method. Even if the method is called on a reference to the base class it will use the implementation overriding it.
// See https://aka.ms/new-console-template for more information
Getraenk getraenk1 = new HeisseMilch(new Schoko(new Espresso()));
Console.WriteLine("{0,-50}{1:0.##}€", getraenk1.GetBeschreibung(), getraenk1.GetPreis());
public abstract class Getraenk
{
public abstract string GetBeschreibung();
public abstract decimal GetPreis();
}
public class Espresso : Getraenk
{
public override string GetBeschreibung() => "Espresso";
public override decimal GetPreis() => 1.99M;
}
public abstract class ZutatenDekorierer : Getraenk;
public class Schoko : ZutatenDekorierer
{
Getraenk _getraenk;
public Schoko(Getraenk getraenk)
{
_getraenk = getraenk;
}
public override string GetBeschreibung() => _getraenk.GetBeschreibung() + "Schoko";
public override decimal GetPreis() => _getraenk.GetPreis() + 0.20M;
}
public class HeisseMilch : ZutatenDekorierer
{
Getraenk _getraenk;
public HeisseMilch(Getraenk getraenk)
{
_getraenk = getraenk;
}
public override string GetBeschreibung() => _getraenk.GetBeschreibung() + "Heisse Milch";
public override decimal GetPreis() => _getraenk.GetPreis() + 0.10M;
}
PS: Geld als Decimal, sonst bekommst Du Rundungsfehler.
Siehe Floating-point numeric types (C# reference) - Real literals
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Wow - vielen, vielen Dank für die schnelle und hilfreiche Antwort! 😃
Dann gehe ich davon aus, dass man in so einem Fall die Code Dopplung, die sich dadurch ergibt (jetzt hab ich ja in jeder Getränke-Klasse die selbe GetBeschreibung Methode, mit dem selben Methoden-Körper drin - wenn ich also neben dem Espresso weitere Getränke erfassen möchte) billigend in Kauf nimmt. Richtig?
Das kommt eben drauf an, was Du insgesamt vor hast - oder ob das hier nur ein Schul-Lernbeispiel ist; ich hab mich an Deinem Code orientiert.
Pattern wie Decorator Pattern sind ja erstmal Ideen; aber je nach Sprache haben diese Varianzen in der Implementierung. In .NET kannst Du den DP via abstract Class (wie Du es hast) umsetzen oder mit einem Interface, oder mit Generics.
Die Implementierung der realen Getränke kann sich des weiteren ebenfalls unterscheiden, zB ob eine Implementierung ein anderes Getränk annimmt, oder nicht. Der DP hier ist aber erfüllt, weil Du auf die jeweilige Parent-Klasse referenzierst (durch das Description). Ob das in einer Eigenschaft oder in einer Methode ist, spielt keine Rolle.
Einen gewissen Overhead / Code-Dopplung kann es ja nach Varianz geben (zB der zwingende Ctor-Parameter).
Der abstrakten Klasse einen Default-Wert zu geben, macht aber in 99,999% der Fälle keinen Sinn - es kann eh keine Instanz von ihr erzeugt werden. Und in Deinem Fall gibt es ja in allen Ableitungen immer einen gewünschten Wert.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Super, Abt, ich denke, ich hab verstanden! Vielen lieben Dank! 😃
Der Code ist übrigens schon am Buch orientiert - aber dort sind die Codebeispiele halt in Java geschrieben! Im Buch war jedenfalls die Idee, dass verschiedene Kaffeearten (also direkte Ableitungen von Getraenk) mit diversen Zutaten "dekorierbar" sein sollen. Beschreibung und Preis sollten sich dann halt entsprechend der Kombination der Dekorationen anpassen.
Naja, und in Java scheint es (zumindest zur Drucklegung des Buches - was ja bald 20 Jahre her ist 😃 ) möglich zu sein, in einer abstrakten Klasse den Code von GetBeschreibung zu hinterlegen, ohne damit einen Effekt zu erhalten, wie ich ihn ja in C# hatte. 😃
Nochmal vielen Dank für Deine Hilfe! 😃
Johann
in Java scheint es möglich zu sein, in einer abstrakten Klasse den Code von GetBeschreibung zu hinterlegen
Das geht in C# genauso, wenn man es entsprechend implementiert, dann eben via optionalem Override. Macht dann auch mehr sinn als ein Zwang-Override mit einem Defaultwert (der nie gezeigt wird).
public abstract class Getraenk
{
public virtual string GetBeschreibung()
{
return "Keine Angabe";
}
public abstract decimal GetPreis();
}
public class Espresso : Getraenk
{
public override decimal GetPreis() => 1.99M;
}
Dann ist die Ausgabe
Keine Angabe Schoko Heisse Milch
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Ich werd verrückt - das war der entscheidende Hinweis, Abt!
Jetzt schaut das perfekt aus und ich hab in meinen Kaffeesorten kein Code Dopplungen mehr! Ich geb halt nur die Property "Beschreibung" zurück, die ich ja in den ctors der Kaffeesorten setze.
public abstract class Getraenk
{
public string Beschreibung { get; set; } = "Unbekanntes Getränk";
public virtual string GetBeschreibung()
{
return Beschreibung;
}
public abstract decimal GetPreis();
}
public class Espresso : Getraenk
{
public Espresso()
{
Beschreibung = "Espresso";
}
public override decimal GetPreis()
{
return 1.99m;
}
}
Ha - wie geil! 😃
Juhu, und weiter geht's zum nächsten Pattern! 😉