Decorator
Adding behaviors without altering the class itself and inheritance.
Motivation
- Want to augment an object with additional functionality.
- Do not want to rewrite or alter existing code(OPC).
- Want to keep new functionality separate(SRP)
- Need to be able to interact with existing structures.
Two options:
- Inherit if not sealed.
- Build a
Decorator
, which simply references the decorated objects.
Delegate an object
For some sealed class, to add new behaviors, we delegate those new functionalities to the object of the original class.
class CustomStringBuilder
{
readonly StringBuilder stringBuilder = new();
// ...delegate some functionalities using stringBuilder
public CustomStringBuilder AppendLine(string s)
{
stringBuilder.AppendLine(s);
return this;
}
}
2
3
4
5
6
7
8
9
10
Multiple Inheritance
C#
does not support Multiple Inheritance, so we may using interface
and default implementation of interface.
interface ICreature
{
int Age { get; set; }
}
interface ICanFly : ICreature
{
void Fly()
{
if (Age > 1)
Console.WriteLine("I'm flying...");
}
}
interface ICanWalk : ICreature
{
void Walk()
{
if (Age < 1)
Console.WriteLine("I'm walking...");
}
}
class Animal : ICanFly, ICanWalk
{
public int Age { get; set; }
}
public class MultipleInheritanceTest
{
[Fact]
public void Test()
{
Animal animal = new() { Age = 1 };
((ICanFly)animal).Fly();
if (animal is ICanWalk a) a.Walk();
}
}
static class CreatureExtension
{
public static void Fly(this ICanFly canFly) => canFly.Fly();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Dynamic Decorator
Dynamic means working in runtime. By offering an abstraction, we can wrap decorator with another decorator, even making it a chain.
interface IShape
{
string AsString();
}
class Shape(double size) : IShape
{
public string AsString() => $"Shape with size {size}";
}
class ShapeWithColor(IShape shape, Color color) : IShape
{
public string AsString() => $"{shape.AsString()} with color {color.Name}";
}
class ShapeWithTransparency(IShape shape, double transparency) : IShape
{
public string AsString() => $"{shape.AsString()} with transparency {transparency}";
}
public class Test
{
[Fact]
public void TestName()
{
Shape shape = new(1.2);
ShapeWithColor withColor = new(shape, Color.Aqua);
ShapeWithTransparency withTransparency = new(withColor, 0.5);
Console.WriteLine(withTransparency.AsString());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Problem - Cycle
We can wrap any type that implements IShape
since we have a instance of IShape
as a field. So we can apply multiple decorators over a simple Shape
object, and this happens during runtime.
But the problem is, this approach may cause a cycle which basically means decorating the instance with the same class because IShape
does not have a constraint. For example:
Shape shape = new(1.2);
ShapeWithColor withColor = new(shape, Color.Aqua);
ShapeWithColor shapeWithColor = new(withColor, Color.Red);
2
3
A shape should not have two colors! It doesn't make sense but neither compiler nor runtime know it.
Solution
To solve the problem, it is only possible to detect it during runtime. There are two scenarios we may make such cycle mistake, one is during construction, another is during invoking. We can check if it is cycled during construction because it is not making sense for sure. If we allow the cycle construction but hide or prevent accessing (string AsString()
for example) when some implementations are invoked, that's the another scenario. So we have two scenarios to solve, we shall make two policies to handle them.
To make two concrete policies, an abstraction is required.
abstract class DecoratorCyclePolicy
{
protected internal abstract bool ShouldAddType(Type type, IList<Type> types);
protected internal abstract bool InvokingAllowed(Type type, IList<Type> types);
}
class ThrowOnConstructionPolicy : DecoratorCyclePolicy
{
private static bool Handle(Type type, IList<Type> types)
{
if (types.Contains(type)) throw new Exception($"type {type.Name} is already in the cycle.");
return true;
}
protected internal override bool ShouldAddType(Type type, IList<Type> types) => Handle(type, types);
protected internal override bool InvokingAllowed(Type type, IList<Type> types) => Handle(type, types);
}
class AllowCyclePolicy : DecoratorCyclePolicy
{
protected internal override bool InvokingAllowed(Type type, IList<Type> types) => true;
protected internal override bool ShouldAddType(Type type, IList<Type> types) => true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Now let's make a generic Decorator
for Shape
class because we do want to include the policy information for each decorator. So we make an abstraction like
abstract class DecoratorForShape : Shape
{
private readonly Shape _shape;
protected readonly List<Type> types = new();
protected DecoratorForShape(Shape shape) : base(shape.size)
{
_shape = shape;
if (shape is DecoratorForShape decorator)
{
types.AddRange(decorator.types);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
What? This is not a generic type! That's because we want to perform implicit casting of pattern matching if (shape is DecoratorForShape decorator)
. If we directly say if (shape is DecoratorForShape<...> decorator)
, that's not gonna work in .NET
, no matter how many arguments required because you didn't offer type parameters.
Now let's make the actual generic decorator
abstract class DecoratorForShape<TSelf, TPolicy> : DecoratorForShape
where TPolicy : DecoratorCyclePolicy, new()
{
protected readonly TPolicy policy = new();
protected DecoratorForShape(Shape shape) : base(shape)
{
if (policy.ShouldAddType(typeof(TSelf), types))
types.Add(typeof(TSelf));
}
}
2
3
4
5
6
7
8
9
10
Since we do want the type of self to be added when the type is not in the cycle as we invoke the corresponding constructor, we set a recursive generic parameter TSelf
.
If you will, you can make an intermediate generic class like
class DecoratorForShapeWithPolicy<TPolicy> : DecoratorForShape<DecoratorForShapeWithPolicy<TPolicy>, TPolicy>
where TPolicy : DecoratorCyclePolicy, new()
{
public DecoratorForShapeWithPolicy(Shape shape) : base(shape) { }
}
2
3
4
5
Though it's not necessary.
Now we can make our concrete decorators!
class ShapeWithColor : DecoratorForShapeWithPolicy<ThrowOnConstructionPolicy>
{
protected readonly Color color;
public ShapeWithColor(Shape shape, Color color) : base(shape)
=> this.color = color;
public new string AsString()
{
StringBuilder sb = new($"{shape.AsString()}");
if (policy.InvokingAllowed(types.First(), types.Skip(1).ToList()))
sb.Append($" with color {color.Name}");
return sb.ToString();
}
}
class ShapeWithTransparency : DecoratorForShapeWithPolicy<ThrowOnConstructionPolicy>
{
protected double transparency;
public ShapeWithTransparency(Shape shape, double transparency) : base(shape)
=> this.transparency = transparency;
public new string AsString()
{
StringBuilder sb = new($"{shape.AsString()}");
if (policy.InvokingAllowed(types.First(), types.Skip(1).ToList()))
sb.Append($" with size {size}");
return sb.ToString();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Static Decorator
Static Decorator is something we defined using generic in a static way, I mean, we don't construct them during runtime, the compiler already knew the concrete decorator for another decorator, something like that. However .NET
is not suitable for doing so, because it doesn't support syntax like
class ShapeWithColor<T> : T where T : Shape { ... } // not available for .NET
ShapeWithColor<ShapeWithTransparency<Shape>> shape = new();
2
Though it's common for template in C++
.
So any decorator for Shape
shall inherits from Shape
, and its generic parameter shall also inherits from Shape
.
abstract class Shape
{
protected internal double size;
protected Shape(double size) => this.size = size;
public Shape() { }
public virtual string AsString() => $"{this.GetType().Name} has size {size}";
}
class Rectangle(double size) : Shape(size)
{
public Rectangle() : this(default) { }
}
class ShapeWithColor<T> : Shape where T : Shape, new()
{
protected internal T Shape { get; init; } = new();
protected internal Color color;
public ShapeWithColor(Color color) : base(default) => this.color = color;
public ShapeWithColor() : base(default) { }
public override string AsString() => $"{Shape.AsString()} with color {color.Name}";
}
class ShapeWithTransparency<T> : Shape where T : Shape, new()
{
protected internal T Shape { get; init; } = new();
protected internal double transparency;
public ShapeWithTransparency(double transparency) : base(default) => this.transparency = transparency;
public ShapeWithTransparency() : base(default) { }
public override string AsString() => $"{Shape.AsString()} with transparency {transparency}";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
It's worth noting that we defined a constructor with no parameter because we do want to perform a nested generic declaration.
ShapeWithTransparency<ShapeWithColor<Rectangle>> shapeWithTransparency = ...;
Why do we use property to specify the instance of T
in such decorator? That's because we don't know the possibility of T
. It may be a Rectangle
, a ShapeWithColor
, a ShapeWithTransparency
or other decorators, their members can be different. So we use property to let users specify it in detail instead of using constructor. However, we can't make sure it is initialized even we have required
keyword, the new()
constraint cannot help compiler know the property of concrete T
, it's not possible in the compile time. So it's rather dangerous and ugly. For those base(default)
, it has nothing dealt with the T Shape
, we just need to reconcile the rule of inheritance.
ShapeWithColor<Rectangle> shapeWithColor = new(color: Color.White) { Shape = new(size: 1) };
ShapeWithTransparency<ShapeWithColor<Rectangle>> shapeWithTransparency = new(transparency: 0.5)
{
Shape = new(Color.AliceBlue)
{
Shape = new(0)
}
};
2
3
4
5
6
7
8