假设我们要扩展同事编写的某个类的某些功能,怎样在不修改类的前提下,增加新的功能呢?有一种方法是使用继承,编写一个继承自该基类的子类,然后添加新的方法,或者重写父类里面的某些方法或属性。
问题在于,在有些情况下,并不能继承。最常见的是这个类无法继承,要么是我们编写的类,需要继承自其他类,而在C#里面,不允许多各类继承,再就是这类是封闭的Sealed,无法继承。
Decorator装饰模式,可以使得我们扩展已经存在的类,而不需要修改已经存在类的代码,并且避免了继承导致产生过多子类。下面用一个例子来说明装饰模式。
自定义字符串构造器
假设我们要做一个代码生成器的功能,需要扩展StringBuilder,来增加缩进功能。首先想到的是直接继承自StringBuilder类,但是出于安全原因,这个类是Sealed封闭类,另外,还要保存当前缩进的级别用来给方法Indent()缩进和UnIndent()取消缩进使用。因为要保存一些状态,所以也不能用静态的扩展方法,因为扩展方法是无状态的。
解决方法就是创建一个装饰器Decorator,一个包含StringBuilder的保存一些额外状态,并提供跟StringBuilder相同的方法的类。看起来如下:
public class CodeBuilder
{
private StringBuilder builder = new StringBuilder();
private int indentLevel = 0;
public CodeBuilder Idndent()
{
indentLevel++;
return this;
}
}
可以看到,现在我们既有StringBuilder类,又有一些额外的字段和扩展方法。现在需要做的就是增加一些StringBuilder里有的一些方法,并将增加的这些方法委托给StringBuilder来处理。StringBuilder是一个很大的API,因此逐个来实现StringBuilder有的方法是不现实的的,通常我们可以使用一些代码生成技术,比如Resharper里面的“生成|委托成员”来自动创建一些必要的API。每一个方法都会生成,代码看起来像这样。
public class CodeBuilder
{
private StringBuilder builder = new StringBuilder();
private int indentLevel = 0;
public CodeBuilder Idndent()
{
indentLevel++;
return this;
}
public StringBuilder Append(String value)
{
return builder.Append(value);
}
public StringBuilder AppendLine()
{
return builder.AppendLine();
}
//TODO: 其他方法在此省略
}
初看起来没问题,但实际上是不正确的。在StringBuilder中,提供的是流式API,使得代码可以写成。
myBuilder.Append("Hello").AppendLine(" World");
但是我们的装饰类却不行,比如说,就不能写成:
myBuilder.Append("x").Indent()
因为在上面的Append方法返回的是StringBuilder,他并没有Indent()方法。所以我们需要做的就是把返回值改为装饰类本身。
public class CodeBuilder
{
public CodeBuilder Append(char value, int repeatCount)
{
builder.Append(value, repeatCount);
return this;
}
}
装饰器作为适配器
我们也可以将装饰器作为一个适配器。比如前面那个例子,假设我们需要CodeBuilder用起来像String,比如,可以想string那样使用+=符号来连接字符串。只需要对CodeBuilder做适当修改即可。
public static implicit operator CodeBuilder(string s)
{
var cb = new CodeBuilder();
cb.builder.Append(s);
return cb;
}
public static CodeBuilder operator +(CodeBuilder cb, string s)
{
cb.Append(s);
return cb;
}
现在CodeBuilder可以像这样用,跟String的用法一样。
CodeBuilder cb = "hello";
cb += " world!";
Console.WriteLine(cb);
多重继承
除了可以扩展sealed封闭类,在C#中不允许多重继承,但装饰器可以实现类似功能。比如,假设我们要对“龙”建模,它既是“鸟”可以飞,也是“蜥蜴”可以爬。
public class Bird
{
public void Fly() { ... }
}
public class Lizard
{
public void Crawl() { ... }
}
我们是不能让Dragon同时继承Bird和Lizard,如下代码编译不通。
public class Dragon:Bird,Lizard
{
}
但是接口就没有多继承的问题,于是我们将上述类改成接口。
public interface Bird
{
void Fly();
}
public interface Lizard
{
void Crawl();
}
现在让Dragon继承自这俩接口,然后将Dragon的Fly和Crawl委托给实现了这两个接口的类。
public class Dragon : IBird, ILizard
{
private readonly IBird bird;
private readonly ILizard lizard;
public Dragon(IBird b, ILizard l)
{
bird = b;
lizard = l;
}
public void Crawl()
{
lizard.Crawl();
}
public void Fly()
{
bird.Fly();
}
}
上述可以通过依赖注入,注入实现了IBird和ILizard的对象。
装饰器存在一个C++中的菱形继承(Diamond Inheritance)问题,现在假设龙在10岁以前只会爬,10岁以后则只会飞,如何建模?在本例中,需要Bird和Lizard都有一个Age属性。
public interface ICreature
{
int Age { get; set; }
}
public interface IBird : ICreature
{
void Fly();
}
public interface ILizard : ICreature
{
void Crawl();
}
public class Bird : IBird
{
public int Age { get; set; }
public void Fly()
{
if (Age >= 10)
{
Console.WriteLine("I am flying");
}
}
}
public class Lizard : ILizard
{
public int Age { get; set; }
public void Crawl()
{
if (Age < 10)
{
Console.WriteLine("I am crawling");
}
}
}
这里引入了ICreature接口,里面包含了Age属性。现在对于Dragon来说,要实现上述两个类,问题在于Age属性该如何设置。
public class Dragon : IBird, ILizard
{
private readonly IBird bird;
private readonly ILizard lizard;
public int Age { get; set; }
public Dragon(IBird b, ILizard l)
{
bird = b;
lizard = l;
}
public void Crawl()
{
lizard.Crawl();
}
public void Fly()
{
bird.Fly();
}
}
由于IBird.Fly()和ILizard.Crawl()方法内部都需要用到Age属性,所以在设置Dragon的Age的时候,必须同时设置IBird和ILizard的Age属性,两者必须保持相等,这样整个Dragon的行为才有一致性。返回的时候,只需要返回IBird和ILizard的Age即可,所以Dragon的Age属性实现如下:
public int Age
{
get => bird.Age;
set => bird.Age = lizard.Age = value;
}
同时在构造函数那里,也必须保证两者相等。
public Dragon(IBird b, ILizard l)
{
bird = b;
lizard = l;
bird.Age = lizard.Age;
}
可以看到,实现装饰器模式非常简单,只需要注意两个地方,一是如果原对象提供了流式接口,那么装饰器对象的时候也需要注意,另外就是“菱形继承问题”,需要保证内部多个对象某些属性的一致性,具体的上面两个例子都有体现。
动态装饰器组合
装饰器也可以进行组合,比如针对同一对象,可能有多个装饰器,或者使用一个装饰器来构造另外一个装饰器。装饰器必须要足够灵活才能实现这一目的。
假设我们有一个抽象的Shape基类,当然也可以定义为接口,只有一个AsString虚属性,用来返回当前对象的形状。
public abstract class Shape
{
public virtual string AsString() => string.Empty;
}
现在我们来定义一些具体类,比如Cricle,Square。
public sealed class Circle : Shape
{
private float radius;
public Circle() : this(0)
{ }
public Circle(float f)
{
radius = f;
}
public void Resize(float factor)
{
radius *= factor;
}
public override string AsString() => $" A circle with radius {radius}";
}
public sealed class Square : Shape
{
private float side;
public Square() : this(0)
{ }
public Square(float f)
{
side = f;
}
public void Resize(float factor)
{
side *= factor;
}
public override string AsString() => $" A square with side {side}";
}
这里创建了两个具体类Circle和Square,这里故意设置为封闭类,这样要添加功能,只有新建装饰器,这里建立两个装饰器。首先是一个颜色装饰器ColorShape,其次是一个透明度装饰器TransparentShape:
public class ColorShape : Shape
{
private readonly Shape shape;
private readonly string color;
public ColorShape(Shape s, string c)
{
shape = s;
color = c;
}
public override string AsString() => $"{shape.AsString()} has color {color}";
}
public class TransparentShape : Shape
{
private readonly Shape shape;
private readonly float transparency;
public TransparentShape(Shape s, float t)
{
shape = s;
transparency = t;
}
public override string AsString() => $"{shape.AsString()} has {transparency * 100.0f} transparency";
}
可以看到这两个装饰器都继承自Shape,他们本身都是Shape,同时在构造函数中又引入了Shape对象,这是装饰器的典型特征。这使得可以像下面这样使用:
Circle c = new Circle(2);
Console.WriteLine(c.AsString());
ColorShape readCircle = new ColorShape(c, "Red");
Console.WriteLine(readCircle.AsString());
TransparentShape readHalfTransparencyCircle = new TransparentShape(readCircle, 0.5f);
Console.WriteLine(readHalfTransparencyCircle .AsString());
输出结果为:
A circle with radius 2
A circle with radius 2 has color Red
A circle with radius 2 has color Red has 50 transparency
可以看到,通过颜色装饰器ColorShape和TransparentShape透明度装饰器,可以将一个Circle对象装饰为“50%透明度红色的圆形”。上面这个有个问题是无法避免循环装饰,比如:
ColorShape bluereadcicle = new ColorShape(new ColorShape(c, "Red"), "Blue");
Console.WriteLine(bluecicle.AsString());
这个能编译能运行,它会产生一个又是红色又是蓝色的圆形,显然逻辑不正确,但是这个系统不知道,也无法阻止这种错误的组合装饰。
这个就是动态装饰器,因为我们是在运行时动态装饰的。像洋葱一样,装饰器一层一层将对象装饰起来。虽然用起来非常方便,但是在装饰完后,他也丢失了类型信息。比如,当我们将Circle装饰为ColorShape后,他是一个ColorShape了,原先Circle里面的Resize方法目前是无法访问到了。
Circle c = new Circle(2);
ColorShape readCircle = new ColorShape(c, "Red");
readCircle.Resize(2);//Error
这个问题无法解决,除非将Resize放在Shape对象中,但是并不是所有的Shape都适合Resize方法。这是动态装饰器的一大缺点。
静态装饰器
在将一个Shape使用ColorShape动态装饰后,我们无法得知其具体的Shape类型,因为我们的构造函数接收的是基类Shape对象,除非调用AsString方法查看其输出。那有没有办法在运行时就得知具体装饰对象的类型呢?有,那就是使用泛型。
思路很简单,装饰器接受一个泛型类型的参数,告知正在装饰的对象的具体类型。在上述的例子中,很显然这个泛型类型必须继承自Shape对象,所以ColorShape和TransparentShape改造成如下泛型类型。
public class ColorShape<T> : Shape where T : Shape, new()
{
private readonly T shape = new T();
private readonly string color;
public ColorShape() : this("Black")
{ }
public ColorShape(string c)
{
color = c;
}
public override string AsString() => $"{shape.AsString()} has color {color}";
}
现在我们有一个泛型的ColorShape,泛型T必须继承自Shape,并且是一个类,支持初始化。现在构造函数中就不用传入Shape了,Shape通过泛型参数传入,注意,这里必须提供默认的构造函数,因为后面方便默认构造。
TransparentShape也按照类似思路改造:
public class TransparentShape<T> : Shape where T : Shape, new()
{
private readonly T shape = new T();
private readonly float transparency;
public TransparentShape() : this(0.0f) { }
public TransparentShape(float t)
{
transparency = t;
}
public override string AsString() => $"{shape.AsString()} has {transparency * 100.0f} transparency";
}
现在,使用静态装饰起来实现先前动态装饰器的内容,如下:
ColorShape<Circle> redCircle = new ColorShape<Circle>("Red");
Console.WriteLine(redCircle.AsString());
TransparentShape<ColorShape<Circle>> halfTransparentBlackShape = new TransparentShape<ColorShape<Circle>>(0.5f);
Console.WriteLine(halfTransparentBlackShape .AsString());
输出结果如下:
A circle with radius 0 has color Red
A circle with radius 0 has color Black has 50 transparency
这种静态装饰器的方式有一些有点和缺点,优点是在经过装饰器后,仍然保留了类型信息。然而,这种方式有一些缺点:
- 可以看到上述例子中,Circle的半径都是0,TransparentShape中,ColorShape<Circle>类型参数,始终是默认构造函数产生的Black黑色。我们在泛型参数中只能提供泛型类型,而不能提供具体的构造信息,比如Circle的半径,ColorShape的黑色。
- 我们仍旧无法在装饰器中访问被构造类型的方法,比如redCircle.Resize(),因为ColorShape<Circle>并没有Resize方法。
- 这种类型的装饰器无法在运行时动态装饰。
总之,缺乏C++里面的奇异递归模板模式(Curiously Recurring Template Pattern,CRTP)和多重继承,C#中的静态装饰器作用非常有限。
总结
装饰器使得我们在不修改一个现行类,遵循OCP开闭原则的基础上,给类增加额外功能,在一定程度上解决了如何扩展封闭类和不能多重继承的问题。装饰器很重要的一个特性是多个装饰器能够以不同顺序进行组合来同时装饰一个对象。装饰器有两种类型:
- 动态装饰器,能够保存对待装饰对象的引用,从而能在运行时进行动态组合装饰。
- 静态装饰器,通过泛型,为装饰过程中提供待装饰对象的类型信
这两种都有限制,那就是无法调用待装饰对象的方法或者属性。另外两只方法都可以循环组合来装饰,这个没有办法可以避免。