结构型模式——装饰器(Decorator)
问题背景
当需要动态地扩展一个对象时,可以考虑使用装饰器。用RPG游戏中烂大街的宝石镶嵌功能举例:现有一个装备类,在设计之初并没有考虑镶嵌功能,如果要在类内部加入镶嵌逻辑,就必须修改原有代码。而修改原有代码很容易引起回归错误,尤其是进行这种大规模改动的时候。
解决方案
由于设计之初没有考虑需求变化,从内部扩展就成了不可能,于是我们考虑在外部扩展。假设待扩展类实现了接口IEquipment,设计一个装饰器类GemEquipment表示镶嵌了宝石的装备,令它也实现IEquipment,并持有一个IEquipment的引用,在使用装饰器时,增加一些处理。这样,原对象和装饰器对用户就是透明的,用户在使用装饰器时的感觉就好像是原对象本身获得了增强一样。使用装饰器后的程序结构是这样的:
值得一提的是,由于GemEquipment也实现接口IEquipment,所以同样可以引用装饰器类。这样,就形成了递归嵌套,通过这种结构,可以无限增强一个对象。同时,这种扩展是针对对象的扩展,而非针对类的扩展,因此具有动态性,支持热插拔,非常灵活。
效果
- 面向接口编程,提高了可扩展性。
- 使用组合,提高了动态性。
- 保证了原对象的静态结构不会过于臃肿,易于维护。
缺陷
装饰器的结构是一个栈而非表,因此必须按照LIFO规则进行插拔,这在一定程度上限制了程序的灵活性。装饰器提供了一定的透明性,但不如复合来的强。装饰器和原对象是有区别的,而复合结构的每个节点都是相同的。如果侧重点是透明性,请用复合。过多的装饰器会造成小对象过多,调用链过长的问题,影响程序性能。
相关模式
- 适配器:适配器和装饰器易混淆,适配器的作用是转换接口,装饰器的作用是动态增强。识别方式就是看目标对象的接口是否发生了变化。
- 复合:装饰器是一个特殊的复合结构,但侧重点是增强。因此,不应该把装饰器当复合用,但可以用装饰器来增强复合结构中的结点。
- 策略:装饰器是在对象外部进行增强,而策略是在对象内部进行增强;装饰器一般用于设计时未考虑的变化,策略一般用于设计时考虑到的变化。
实现
using System;
namespace Decorator
{
class Client
{
public interface IEquipment
{
int ATK { get; }
int DEF { get; }
void Show();
}
public class Equipment : IEquipment
{
public int ATK { get; }
public int DEF { get; }
public Equipment(int atk, int def)
{
ATK = atk;
DEF = def;
}
public void Show()
{
Console.WriteLine($"攻击力: {ATK}, 防御力: {DEF}");
}
}
public class GemEquipment : IEquipment
{
private IEquipment target;
private Gem gem;
private int atk;
private int def;
public GemEquipment(IEquipment target, Gem gem)
{
this.target = target;
this.gem = gem;
}
public int ATK
{
get
{
return gem.ATK + target.ATK;
}
}
public int DEF
{
get
{
return gem.DEF + target.DEF;
}
}
public void Show()
{
Console.WriteLine($"攻击力: {ATK}, 防御力: {DEF}");
}
}
public class Gem
{
public Gem(int atk, int def)
{
ATK = atk;
DEF = def;
}
public int ATK { get; }
public int DEF { get; }
}
static void Main(string[] args)
{
Console.WriteLine("创建装备...");
IEquipment equipment = new Equipment(100, 50);
equipment.Show();
Console.WriteLine("镶嵌宝石...");
equipment = new GemEquipment(equipment, new Gem(25, 0));
equipment.Show();
Console.WriteLine("再来一颗...");
equipment = new GemEquipment(equipment, new Gem(10, 10));
equipment.Show();
}
}
}