Reputation: 113
I was studying the "Decorator pattern". Doing some tests with C# I do not understand why I do not get the expected result. This is the code:
public abstract class Drink
{
public string description = "Generic drink";
public string GetDescription()
{
return description;
}
}
public abstract class DrinkDecorator : Drink
{
public abstract string GetDescription();
}
public class SecretIngredient : DrinkDecorator
{
Drink drink;
public SecretIngredient (Drink drink)
{
this.drink = drink;
}
public override string GetDescription()
{
return this.drink.GetDescription() + ", SecretIngredient ";
}
}
public class Espresso : Drink
{
public Espresso()
{
description = "Espresso";
}
}
[TestFixture]
class TestClass
{
[Test]
public void TestMethod()
{
Drink drink = new Espresso();
System.Diagnostics.Debug.WriteLine(drink.GetDescription());
drink = new SecretIngredient (drink);
System.Diagnostics.Debug.WriteLine(drink.GetDescription());
}
}
Performing the test I get:
Espresso
Generic drink
While I would have expected:
Espresso
Espresso, SecretIngredient
Why? Thanks in advance.
Upvotes: 1
Views: 288
Reputation: 6749
This is because you have Drink
declared as a Drink
type.
Before you read my explanation; if you do this to your code it will work and I try to explain why below:
System.Diagnostics.Debug.WriteLine(((SecretIngredient)drink).GetDescription());
When you assign a Type
to the reference then this Type
is the fallback for metadata. In other words, whatever fields, methods, properties, the Type
has (or has inherited) is what is used; nothing above.
Here we have a simple Person
that is also the base of an Employee
. Look at the output and follow the type declarations.
using System;
namespace Question_Answer_Console_App
{
class Program
{
static void Main(string[] args)
{
Person person = new Person() { Name = "Mathew" };
Person employeePerson = new Employee() { Name = "Mark" };
Person castedEmployee = new Employee() { Name = "Luke" };
Employee employee = new Employee() { Name = "John" };
//Compile error -> Employee personEmployee = new Person() { Name = "Acts" };
Console.WriteLine(person.Name);
Console.WriteLine(employeePerson.Name); //Referenced Employee but got Person
Console.WriteLine(((Employee)castedEmployee).Name); //Notice we cast here
Console.WriteLine(employee.Name);
Console.ReadKey();
}
}
public class Person
{
public string Name { get; set; } = "No Name";
}
public class Employee : Person
{
new public string Name { get; set; }
public string Address { get; set; }
}
//Output
//Mathew
//No Name
//Luke
//John
}
Ok, so if you were able to follow that and make sense of how Type
metadata is used then now you need to look at this with interface
. It's the same thing but we can have an awkward twist.
With in interface
it is possible for two interfaces to have the same properties or methods or even property and method names but different logic. When a Type
uses more than one interface
and they share any of these but the logic required is different we then need to explicitly declare that interface members. However; when we do this then we ONLY get to use those members when the Type
is references as such. Look at this similar example:
First notice that now 'Luke' (previously this was Mark but same logic) prints... Why is that when we are referencing a Person
but it's instantiated as Employee
. Before this didn't work.
Also notice there's a hole in the output although the member was defined; however in this case our reference is to IEmployee
when that happens. Play with all of this code for a while until it sinks in because it can be a big problem later.
using System;
namespace Question_Answer_Console_App
{
class Program
{
static void Main(string[] args)
{
IPerson iPerson = new Person() { Name = "Mathew" };
Person person = new Person() { Name = "Mark" };
Person employeePerson = new Employee() { Name = "Luke" }; //pay attention to this!!
IPerson iEmployeePerson = new Employee() { Name = "John" };
IEmployee iEmployee = new Employee() { Name = "Acts" }; //And pay attention to this!!
Employee employee = new Employee() { Name = "Romans" };
Console.WriteLine(iPerson.Name);
Console.WriteLine(person.Name);
Console.WriteLine(employeePerson.Name);
Console.WriteLine(iEmployeePerson.Name);
Console.WriteLine(iEmployee.Name);
iEmployee.Name = "Corinthians"; //And pay attention to this!!
Console.WriteLine(iEmployee.Name);
Console.WriteLine(employee.Name);
Console.ReadKey();
}
}
public interface IPerson
{
string Name { get; set; }
}
public interface IEmployee
{
string Name { get; set; }
}
public class Person : IPerson
{
public string Name { get; set; } = "No Name";
}
public class Employee : Person, IEmployee
{
public string Address { get; set; }
string IEmployee.Name { get; set; } //And pay attention to this!! (Explicit interface declaration)
}
//Output
//Mathew
//Mark
//Luke
//John
//Corinthians
//Romans
}
Now; if you understand that so far then let's look at ways to get around it. Take the first example: If you add virtual
to Name property of Person
and then use override
in the Name property of Employee
you'll see that the types now work as expected. This is because we are not referencing two different methods. We are marking one with the ability to be re-referenced (virtual) and another to reference it (override). This changes the behavior greatly.
So all that said and understood then let's make a proper decorator.
First; we need to have a type:
public class Person
{
public virtual string Name { get; set; } = "John Doe";
}
Now we need types that have extended functionality... (this will need to be altered later)
public class Employee : Person
{
public override string Name => $"Employee, {base.Name}";
public string Job { get; set; }
}
public class Customer : Person
{
public override string Name => $"Customer, {base.Name}";
public bool IsShopping { get; set; }
}
Now it is possible for an employee to also be a customer. Based on our current design we have a problem... We should have added interfaces but then what about computation? In this example there isn't any, except for Name, which isn't real world but it does it's job. So in order to allow Person
to be dynamically updated we should add a PersonDecorator
. When we add this decorator we need to inherit from it and use other types to instantiate.
Here's our decorator:
public abstract class PersonDecorator : Person
{
protected Person Person { get; }
public PersonDecorator(Person person) => Person = person;
public override string Name => Person.Name;
}
Now we can extend Person
dynamically where we couldn't before. Upgrading Employee
and Customer
shows how to do this:
public class Employee : PersonDecorator
{
public Employee(Person person = null) : base(person ?? new Person()) { }
public override string Name => $"Employee, {base.Name}";
public string Job { get; set; }
}
public class Customer : PersonDecorator
{
public Customer(Person person) : base(person ?? new Person()) { }
public override string Name => $"Customer, {base.Name}";
public bool IsShopping { get; set; }
}
Now we have upgraded our types to utilize a decorator (and notice it has fallback to types that may not). Let's use it in a small example:
static void Main(string[] args)
{
Person person = new Person() { Name = "Mathew" };
Console.WriteLine(person.Name);
person = new Employee(person) { Job = "Stocker" };
Console.WriteLine(person.Name);
person = new Customer(person) { IsShopping = true };
Console.WriteLine(person.Name);
Console.ReadKey();
}
//OUTPUTS
//Mathew
//Employee, Mathew
//Customer, Employee, Mathew
Notice how we have now dynamically extended Person
.
We could also make person dynamic like so:
static void Main(string[] args)
{
Person person = new Customer(new Employee(new Person(){ Name = "Mathew" }){ Job = "Stocker" }){ IsShopping = true };
Console.WriteLine(person.Name);
Console.ReadKey();
}
//OUTPUTS
//Customer, Employee, Mathew
Look how it works without the base implemented first; it remains dynamic and true to itself.
static void Main(string[] args)
{
//Person person = new Person() { Name = "Mathew" };
//Console.WriteLine(person.Name);
Person person = new Employee() { Job = "Stocker" };
Console.WriteLine(person.Name);
person = new Customer(person) { IsShopping = true }
Console.WriteLine(person.Name);
Console.ReadKey();
}
//OUTPUTS
//Employee, John Doe
//Customer, Employee, John Doe
Here's the entire code for reference Decorator Pattern
using System;
namespace Question_Answer_Console_App
{
class Program
{
static void Main(string[] args)
{
Person person = new Person() { Name = "Mathew" };
Console.WriteLine(person.Name);
person = new Employee(person) { Job = "Stocker" };
Console.WriteLine(person.Name);
person = new Customer(person) { IsShopping = true };
Console.WriteLine(person.Name);
Console.ReadKey();
}
//OUTPUTS
//Mathew
//Employee, Mathew
//Customer, Employee, Mathew
}
public class Person
{
public virtual string Name { get; set; } = "John Doe";
}
public abstract class PersonDecorator : Person
{
protected Person Person { get; }
public PersonDecorator(Person person) => Person = person;
public override string Name => Person.Name;
}
public class Employee : PersonDecorator
{
public Employee(Person person = null) : base(person ?? new Person()) { }
public override string Name => $"Employee, {base.Name}";
public string Job { get; set; }
}
public class Customer : PersonDecorator
{
public Customer(Person person) : base(person ?? new Person()) { }
public override string Name => $"Customer, {base.Name}";
public bool IsShopping { get; set; }
}
}
Here's your code updated to the decorator pattern. Notice how you can dynamically update Drink
which was an Expresso
by adding it to decorators.
using System;
namespace Question_Answer_Console_App
{
class Program
{
static void Main(string[] args)
{
Drink drink = new Espresso() { Description = "Expresso" };
Console.WriteLine(drink.Description);
drink = new SecretIngredient(drink);
Console.WriteLine(drink.Description);
drink = new Ice(drink);
Console.WriteLine(drink.Description);
Console.ReadKey();
}
//OUTPUTS
//Expresso
//Expresso with SecretIngredient
//Expresso with SecretIngredient with Ice
}
public class Drink
{
public virtual string Description { get; set; }
}
public class Espresso : Drink { }
public abstract class DrinkDecorator : Drink
{
protected Drink drink;
protected DrinkDecorator(Drink drink) => this.drink = drink;
public override string Description => drink.Description;
}
public class SecretIngredient : DrinkDecorator
{
public SecretIngredient(Drink drink) : base(drink) { }
public override string Description => $"{drink.Description} with {nameof(SecretIngredient)} ";
}
public class Ice : DrinkDecorator
{
public Ice(Drink drink) : base(drink) { }
public override string Description => $"{drink.Description} with {nameof(Ice)} ";
}
}
Upvotes: 1
Reputation: 660034
The fundamental problem is that you have not implemented the decorator pattern correctly. A correct implementation of the decorator pattern would be as follows (and we'll fix a bunch more stuff while we're at it.)
public abstract class Drink
{
// Fields should be private or protected, but the
// description field that was here is useless, and
// even if it were here, it should be a constant,
// not a variable
// Eliminate it.
// public string description = "Generic drink";
// Things that are logically properties should be
// properties, not GetBlah methods.
// In new versions of C# you can use compact syntax
// for properties.
// In the decorator pattern the behaviour mutated by the
// decorator should be virtual.
public virtual string Description => "generic drink";
}
public abstract class DrinkDecorator : Drink
{
// The decorator must override the underlying implementation.
public abstract override string Description { get; }
}
public class SecretIngredient : DrinkDecorator
{
Drink drink;
public SecretIngredient (Drink drink)
{
this.drink = drink;
}
// Use interpolation.
public override string Description =>
$"{this.drink.Description}, SecretIngredient ";
}
public class Espresso : Drink
{
public Espresso()
{
// This is just wrong. We have a mechanism for overriding
// behaviour so **use it**.
// description = "Espresso";
}
public override string Description => "Espresso";
}
[TestFixture]
class TestClass
{
[Test]
public void TestMethod()
{
Drink drink = new Espresso();
System.Diagnostics.Debug.WriteLine(drink.Description);
drink = new SecretIngredient (drink);
System.Diagnostics.Debug.WriteLine(drink.Description);
}
}
And now the correct implementation has the expected output.
The reason your wrong implementation got the wrong output was because it was wrong. You had two entirely separate implementations of GetDescription
that had nothing to do with each other, one implemented by Drink
and the other by the decorator, so which one got called depended on the compile-time type of the receiver, which was Drink
.
You should have gotten a warning saying that you had a possibly unintentional hiding of an old method by a new method. Pay attention to those warnings. If you get a warning saying "this method is probably wrong" and then you get a wrong result when you call that method, the warning was right.
Upvotes: 2