步步为营 .NET 设计模式学习笔记 十四、Decorator(装饰模式)
概述
在软件系统中,有时候我们会使用继承来扩展对象的功能,但是由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。如何使“对象功能的扩展”能够根据需要来动态地实现?同时避免“扩展功能的增多”带来的子类膨胀问题?从而使得任何“功能扩展变化”所导致的影响将为最低?这就是本文要讲的Decorator模式。
一个场景是我们要为一个对象动态添加新的职责,这个职责并不修改原有的行为,而是在原有行为基础上添加新的功能,就好比装饰工人为一座新居的墙上涂抹上色彩缤纷的颜料一般。
意图
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。[GOF 《设计模式》]
<Design Pattern>结构图
图1 Decorator模式结构图
在装饰模式中的各个角色有:
- 抽象构件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。
- 具体构件(Concrete Component)角色:定义一个将要接收附加责任的类。
- 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
- 具体装饰(Concrete Decorator)角色:负责给构件对象"贴上"附加的责任。
生活中的例子
装饰模式动态地给一个对象添加额外的职责。不论一幅画有没有画框都可以挂在墙上,但是通常都是有画框的,并且实际上是画框被挂在墙上。在挂在墙上之前,画可以被蒙上玻璃,装到框子里;这时画、玻璃和画框形成了一个物体。
图2 使用有画框的画作为例子的装饰模式对象图
示例用例图
在超市里,平常节日都有优惠活动,不同节日有不同优惠活动,我们用装饰模式写一个超市节日打折的例子,用例图如下:
代码设计
先创建SuperMarket.cs:
public abstract class SuperMarket { private int _CategoryNumber; public int CategoryNumber { get { return _CategoryNumber; } set { _CategoryNumber = value; } } private string _Name; public string Name { get { return _Name; } set { _Name = value; } } private double _Discount; public double Discount { get { return _Discount; } set { _Discount = value; } } private string _Festival; public string Festival { get { return _Festival; } set { _Festival = value; } } public abstract string ShowInfo(); }
再创建Decorator.cs:
public abstract class Decorator : SuperMarket { protected SuperMarket superMarker; public Decorator(SuperMarket superMarker) { this.superMarker = superMarker; } public override string ShowInfo() { return superMarker.ShowInfo(); } }
再创建WalMart .cs:
public class WalMart:SuperMarket { public WalMart() { this.CategoryNumber = 1000; this.Discount = 0.95; this.Name = "沃尔玛"; this.Festival = "平常"; } public override string ShowInfo() { StringBuilder strBuilder = new StringBuilder(); strBuilder.AppendFormat("在{0}超市,{1}节日期间,折扣为{2}的商品种类有{3}类.\n", this.Name,this.Festival, this.Discount, this.CategoryNumber); return strBuilder.ToString(); } }
再创建MidAutumnFestival.cs:
public class MidAutumnFestival : Decorator { public MidAutumnFestival(SuperMarket superMarket) : base(superMarket) { } public void Preferential() { superMarker.Discount = 0.95; superMarker.CategoryNumber = 1500; superMarker.Festival = "中秋节"; } public string MoonCake() { return "期间所有月饼8折优惠"; } }
再创建SpringFestival.cs:
public class SpringFestival : Decorator { public SpringFestival(SuperMarket supermarket) : base(supermarket) { } public void Preferential() { superMarker.Discount = 0.9; superMarker.CategoryNumber = 2100; superMarker.Festival = "春节"; } public string GiftPreferential() { return "期间有100种礼品类商品8折优惠."; } }
最后再调用:
public partial class Run : Form { public Run() { InitializeComponent(); } private void btnRun_Click(object sender, EventArgs e) { //------------------------------------- SuperMarket superMarket = new WalMart(); rtbResult.AppendText(superMarket.ShowInfo()+"\n"); SpringFestival springFestival = new SpringFestival(superMarket); springFestival.Preferential(); rtbResult.AppendText(springFestival.ShowInfo()); rtbResult.AppendText(springFestival.GiftPreferential()+"\n\n"); MidAutumnFestival midAutumnFestival = new MidAutumnFestival(superMarket); midAutumnFestival.Preferential(); rtbResult.AppendText(midAutumnFestival.ShowInfo()); rtbResult.AppendText(midAutumnFestival.MoonCake() + "\n\n"); } }
结果如下图:
效果及实现要点
1.Component类在Decorator模式中充当抽象接口的角色,不应该去实现具体的行为。而且Decorator类对于Component类应该透明,换言之Component类无需知道Decorator类,Decorator类是从外部来扩展Component类的功能。
2.Decorator类在接口上表现为is-a Component的继承关系,即Decorator类继承了Component类所具有的接口。但在实现上又表现为has-a Component的组合关系,即Decorator类又使用了另外一个Component类。我们可以使用一个或者多个Decorator对象来“装饰”一个Component对象,且装饰后的对象仍然是一个Component对象。
3.Decortor模式并非解决“多子类衍生的多继承”问题,Decorator模式的应用要点在于解决“主体类在多个方向上的扩展功能”——是为“装饰”的含义。
4.对于Decorator模式在实际中的运用可以很灵活。如果只有一个ConcreteComponent类而没有抽象的Component类,那么Decorator类可以是ConcreteComponent的一个子类。
如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。
5.Decorator模式的优点是提供了比继承更加灵活的扩展,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。
6.由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是,在另一方面,使用装饰模式会产生比使用继承关系更多的对象。更多的对象会使得查错变得困难,特别是这些对象看上去都很相像。
7.让装饰角色还继承抽象构件角色也是装饰模式最大的特点,目的就是给抽象构件增加职责,对外表现为装饰后的构件。
8.让装饰角色拥有构件角色实例的目的就是让构件能被多个装饰对象来装饰。
9.在具体应用中可以灵活一点,不一定要有抽象构件和装饰角色。但是,装饰对象继承装饰对象并且拥有它实例的两大特点需要体现。
10.透明装饰一般通过在基类方法前后进行扩充实现,半透明装饰一般通过新的接口实现。
适用性
1.需要扩展一个类的功能,或给一个类增加附加责任。
2.需要动态地给一个对象增加功能,这些功能可以再动态地撤销。
3.需要增加由一些基本功能的排列组合而产生的非常大量的功能,从而使继承关系变得不现实。
4.从代码角度来说,如果你觉得由于功能的交叉扩展不会导致非常多的子类或者非常多的继承层次的话可以考虑装饰模式。
5.从应用角度来说,如果你希望动态给类赋予或撤销一些职责,并且可以任意排列组合这些职责的话可以使用装饰模式。
优点
1.装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
2.通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。
3.这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错。
缺点
1.由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是,在另一方面,使用装饰模式会产生比使用继承关系更多的对象。更多的对象会使得查错变得困难,特别是这些对象看上去都很相像。
注意点
1.一个装饰类的接口必须与被装饰类的接口相容。
2.尽量保持Component作为一个"轻"类,不要把太多的逻辑和状态放在Component类里。
3.如果只有一个ConcreteComponent类而没有抽象的Component类(接口),那么Decorator类经常可以是ConcreteComponent的一个子类。如下图所示:
4.如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。
总结
Decorator模式采用对象组合而非继承的手法,实现了在运行时动态的扩展对象功能的能力,而且可以根据需要扩展多个功能,避免了单独使用继承带来的“灵活性差”和“多子类衍生问题”。同时它很好地符合面向对象设计原则中“优先使用对象组合而非继承”和“开放-封闭”原则。