Decorator Pattern (装饰模式)
装饰模式可「动态」地给一个对象添加一些额外的职责,提供有别于「继承」的另一种选择。就扩展功能而言,Decorator Pattern 透过 Aggregation (聚合) 的特殊应用,降低了类与类之间的耦合度,会比单独使用「继承」生成子类更为灵活。
一般用「继承」来设计子类的做法,会让程序变得较僵硬,其对象的行为,是在「编译」时期就已经「静态」决定的,而且所有的子类,都会继承到相同的行为;然而,若用「装饰模式」以及 UML 的 Aggregation 的设计,来扩展对象的行为,就能弹性地 (flexible) 将多个「装饰者」混合着搭配使用,而且是在「执行」时期「动态」地进行扩展。
此外,若用一般「继承」的做法,每当对象需要新行为时,必须修改既有的代码、重新编译;但若透过「装饰模式」,则无须修改既有代码。
The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
- Design Patterns: Elements of Reusable Object-Oriented Software
图 1 此图为 Decorator 模式的经典 Class Diagram
01_Shell / Program.cs
using System;
namespace _01_Shell
{
//客户端程序
class Program
{
static void Main(string[] args)
{
ConcreteComponent c = new ConcreteComponent();
ConcreteDecoratorA d1 = new ConcreteDecoratorA();
ConcreteDecoratorB d2 = new ConcreteDecoratorB();
//让 ConcreteDecorator 在运行时,动态地给 ConcreteComponent 增加职责
d1.SetComponent(c);
d2.SetComponent(d1);
d2.Operation();
Console.Read();
}
}
//装饰者、被装饰者的共同基类
abstract class Component
{
public abstract void Operation();
}
//被装饰者。具体被装饰对象。
//此为在「执行」时期,会被 ConcreteDecorator 具体装饰者,「动态」添加新行为(职责)的对象
class ConcreteComponent : Component
{
public override void Operation()
{
Console.WriteLine("具体被装饰对象的操作");
}
}
//装饰者。
//此为所有装饰者,应共同实现的抽象类或接口
abstract class Decorator : Component
{
//以父类声明的字段。
//实现 UML Class Diagram 的 Aggregation,指向 Component 对象的指针。
protected Component component;
public void SetComponent(Component component)
{
this.component = component;
}
public override void Operation()
{
if (component != null)
{
component.Operation();
}
}
}
//具体装饰者 A
//此为在「执行」时期,替 ConcreteComponent「动态」添加新行为(职责)的对象
class ConcreteDecoratorA : Decorator
{
//装饰者可以添加新的栏位
private string addedState;
public override void Operation()
{
base.Operation();
addedState = "New State";
Console.WriteLine("具体装饰对象 A 的操作");
}
}
//具体装饰者 B
//此为在「执行」时期,替 ConcreteComponent「动态」添加新行为(职责)的对象
class ConcreteDecoratorB : Decorator
{
public override void Operation()
{
base.Operation();
AddedBehavior();
Console.WriteLine("具体装饰对象 B 的操作");
}
//装饰者可以添加新的方法
private void AddedBehavior()
{
}
}
} // end of namespace
/*
执行结果:
具体被装饰对象的操作
具体装饰对象 A 的操作
具体装饰对象 B 的操作
*/
上方图 1 的 Class Diagram,以及「Shell (壳)」示例中,ConcreteComponent 即为此一模式中的「被装饰者」,而 Decorator 抽象类及其具体子类 ConcreteDecoratorA、ConcreteDecoratorB 即为「装饰者」。此外,这个模式中最核心的就是 Decorator 这个抽象类,它用一个以父类 Component 声明的变量 component,实现 UML 中的 Aggregation (聚合,有的博文也统称为「组合」或「合成」),亦即为指向 Component 对象的指针,达到我们前述 - 「动态」进行扩展的目的。
至于其设计的原理,以下我引用博客园一位前辈 Justin 两年前所写博文的一段内容 [1],这段是我认为对 Decorator 模式极优的说明。读者亦可搭配参考上方代码里我添加的注释,稍后我会再补充说明。
--------------------------------------------------------
Decorator 是装饰者模式里非常特殊的一个类,它既继承于 Component【IS A关系】,又维护一个指向 Component 实例的引用【HAS A关系】,换个角度来说,Decorator 跟 Component 之间,既有动态组合关系,又有静态继承关系,WHY? 这里为什么要这么来设计?上面我们说过,组合的好处是可以在运行时给对象增加职责,Decorator【HAS A】Component 的目的,是让 ConcreteDecorator 可以在运行时动态地给 ConcreteComponent 增加职责,这一点相对来说还比较好理解;那么 Decorator 继承于 Component 的目的是什么?在这里继承的目的只有一个,那就是可以统一「装饰者」和「被装饰者」的接口,换个角度来说,不管是 ConcretComponent 还是 ConcreteDecorator,它们都是最顶层 Component 基类,用户代码可以把它们统一看作 Component 来处理,这样带来的更深一层好处就是,「装饰者」对象,对「被装饰者」对象的功能职责扩展,对用户代码来说是完全透明的,因为用户代码引用的都是 Component,所以就不会因为「被装饰者」对象在被装饰后,引用它的用户代码发生错误,实际上不会有任何影响,因为装饰前后,用户代码引用的都是 Component 类型的对象,这真是太完美了!「装饰模式」通过继承,实现统一了「装饰者」和「被装饰者」的接口,通过组合获得了在运行时动态扩展「被装饰者」对象的能力。
我们再举个生活中的例子,俗话说“人在衣着马在鞍”,把这用「装饰模式」的语境翻译一下,“人通过漂亮的衣服装饰后,男人变帅了,女人变漂亮了;”。对应上面的类图,这里「人」对应于 ConcreteComponent,而「漂亮衣服」则对应于 ConcreteDecorator;换个角度来说,人和漂亮衣服组合在一起【HAS A】,有了帅哥或美女,但是他们还是人【IS A】,还要做人该做的事情,但是可能会对异性更有吸引力了(扩展功能)!
--------------------------------------------------------
上方 Justin 前辈,其文章的「煮咖啡」示例 [1],是引用自 O'Reilly 出版社的「Head First 设计模式」这本书的第三章 [10]。该文的煮咖啡类图中,HouseBlend (家常咖啡)、DarkRoast (深度烘培咖啡)、Espresso (意大利特浓咖啡)、Decaf (无咖啡因咖啡),这四种咖啡 (饮料),即为「被装饰者」,等同本帖上图 1 中的 ConcreteComponent 类;该文类图中的 CondimentDecorator 抽象类,等同本帖上图 1 中最重要的 Decorator 抽象类,亦即「装饰者」的抽象定义;该文类图中的 Milk、Mocha、Soy、Whip 这四种调料 (调味品),即为具体的「装饰者」,亦即在本帖一开始提到,这四种调料,可弹性地 (flexible) 混合着搭配使用,而且是在「执行」时期「动态」地进行扩展,亦即动态地装饰 HouseBlend、DarkRoas、Espresso、Decaf 这四种咖啡。
接下来,我们用另一个简单的例子来实现 Decorator 模式,并改用 ASP.NET 网页程序来实现。类图及代码如下方图 2 所示,执行结果如下图 3 所示。
此为一个西餐牛排馆的计费程序,这间牛排馆有两种主菜 - 牛排和猪排,此为「被装饰者」;有四种副菜 - 面条、生菜沙拉、饮料 (热饮或冷饮)、甜点,此为「装饰者」。客人点餐时,可点一种主菜,副菜可点一份、可点多份,也可不点 (有弹性地将多个「装饰者」混合着搭配);每样主菜和副菜都有各自的价格,全部相加后,即为一或多位客人的用餐费用。而且我们希望,将来这个计费程序写好后,未来即使要修改价格,或添加新的主菜和副菜时,都不用再修改既有的程序。
图 2 示例 02_Steak.aspx.cs 的 Class Diagram
02_Steak.aspx.cs
using System;
using com.cnblogs.WizardWu.sample02; //客端程序调用服务器端的组件和类
//客端程序
public partial class _02_Steak : System.Web.UI.
Page
{
protected void Page_Load(object sender, EventArgs e)
{
//点一客猪排,不需要副菜。并列出它的描述与价格
Meal meal1 = new PorkChop();
//Meal meal1 = new BeefSteak();
Response.Write(meal1.Description + " = $ " + meal1.cost() + "
");
//点一客猪排,副菜只要面条。并列出它的描述与价格
Meal meal2 = new PorkChop();
meal2 = new Noodle(meal2); //用 Noodle 装饰它 (运行时动态地增加职责)
Response.Write(meal2.Description + " = $ " + meal2.cost() + "
");
//点一客猪排,因为这个人食量大,副菜要两份面条、一杯饮料(热饮)、一盘甜点。并列出它的描述与价格
Meal meal3 = new PorkChop();
meal3 = new Noodle(meal3); //用 Noodle 装饰它
meal3 = new Noodle(meal3); //用第二个 Noodle 装饰它
meal3 = new Drink(meal3, true); //用 Drink 装饰它
meal3 = new Dessert(meal3); //用 Dessert 装饰它
Response.Write(meal3.Description + " = $ " + meal3.cost() + "
");
//第四个人不吃猪肉,因此主菜改点一客牛排,副菜要一盘沙拉、一杯饮料(冷饮)。并列出它的描述与价格
Meal meal4 = new BeefSteak();
meal4 = new Salad(meal4); //用 Salad 装饰它 (运行时动态地增加职责)
meal4 = new Drink(meal4, false); //用 Drink 装饰它
Response.Write(meal4.Description + " = $ " + meal4.cost() + "
");
}
}
//服器端程序
namespace com.cnblogs.WizardWu.sample02
{
//装饰者(副菜)、被装饰者(主菜)的共同基类。类似前一例的 Component 抽象类
abstract class Meal
{
protected string description = "西餐";
public virtual string Description
{
get { return description; }
set { description = value; }
}
abstract public int cost(); //必须在子类实作的抽象方法,除非该个子类也是抽象类
}
//主菜(被装饰者)。类似前一例的 ConcreteComponent 类
class PorkChop : Meal //主菜 - 猪排。以后执行时,才会动态被副菜装饰
{
public PorkChop() //构造函数
{
Description = "猪排"