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 = "猪排";
}
public override int cost() //具体对象的操作
{
return 130; //猪排的价格。现在不需要管「副菜」的价格,直接返回猪排的价格即可
}
}
//主菜(被装饰者)。类似前一例的 ConcreteComponent 类
class BeefSteak : Meal //主菜 - 牛排。以后执行时,才会动态被副菜装饰
{
public BeefSteak() //构造函数
{
Description = "牛排";
}
public override int cost() //具体对象的操作
{
return 150; //牛排的价格。现在不需要管「副菜」的价格,直接返回牛排的价格即可
}
}
//装饰者。类似前一例的 Decorator 抽象类。此为所有「副菜(装饰者)」的父类。
//功能一、未来所有可能添加的副菜,其共同的属性、行为,都可放在这个类里。
//功能二、用来「分类」哪些是主菜,哪些是副菜。
abstract class CondimentDecorator : Meal //副菜,用来「动态」装饰主菜和 Meal 基类,亦即在运行时动态地替他们增加职责
{
//以父类声明的字段。
//实现 UML Class Diagram 的 Aggregation,指向 Meal 对象的指针。
protected Meal meal;
public override string Description
{
get { return meal.Description + " + " + description; }
set { description = value; }
}
}
//副菜(装饰者)。具体装饰类 - 面条,用来动态装饰主菜 - 牛排或猪排
class Noodle : CondimentDecorator //类似前一例的 ConcreteDecoratorA
{
public Noodle(Meal meal) //构造函数
{
Description = "面条";
this.meal = meal;
}
public override int cost() //具体装饰对象 A 的操作
{
return 50 + meal.cost();
}
}
//副菜(装饰者)。具体装饰类 - 沙拉,用来动态装饰主菜 - 牛排或猪排
class Salad : CondimentDecorator //类似前一例的 ConcreteDecoratorB
{
public Salad(Meal meal) //构造函数
{
Description = "生菜沙拉";
this.meal = meal;
}
public override int cost() //具体装饰对象 B 的操作
{
return 60 + meal.cost();
}
}
//副菜(装饰者)。具体装饰类 - 饮料,用来动态装饰主菜 - 牛排或猪排
class Drink : CondimentDecorator //类似前一例的 ConcreteDecoratorC
{
private bool isHot;
public bool IsHot
{
get { return isHot; }
}
public Drink(Meal meal, bool isHot) //构造函数
{
Description = (isHot) ? "热饮" : "冷饮";
this.meal = meal;
this.isHot = isHot;
}
public override int cost() //具体装饰对象 C 的操作。冷饮比热饮多收 20 块钱
{
return (isHot) ? (30 + meal.cost()) : (50 + meal.cost());
}
}
//副菜(装饰者)。具体装饰类 - 甜点,用来动态装饰主菜 - 牛排或猪排
class Dessert : CondimentDecorator //类似前一例的 ConcreteDecoratorD
{
public Dessert(Meal meal) //构造函数
{
Description = "甜点";
this.meal = meal;
}
public override int cost() //具体装饰对象 D 的操作
{
return 40 + meal.cost();
}
}
} // end of namespace
图 3 示例 02_Steak.aspx.cs 的执行结果。扩展功能时,主菜和副菜可任意混合搭配使用
原理与本帖之前提过的相同,将来店里若要推出 羊排、鱼排、…等新的主菜 (被装饰者) 时,只要增加一个类,去继承 Meal 这个抽象的基类即可;若要推出新的副菜 (装饰者) 也一样,只要增加一个类,去继承 CondimentDecorator 抽象类即可。我们透过这个重要的 CondimentDecorator 抽象类,实现 UML 的 Aggregation (聚合) 观念,以「装饰模式」取代传统「继承」的做法,也同时降低了「主菜」、「副菜」这两种类之间的耦合度。
这个 CondimentDecorator 抽象类的作用,还可将未来所有种类的副菜,一些「共同的」行为或职责写在里面。而且,它还可用来区分哪些类是主菜,哪些类是副菜,因为主菜、副菜都是继承自 Meal 这个最顶层的基类,若没有这个 CondimentDecorator 抽象类,将来 Class Diagram 变得很复杂时,会不易分辨某个类是主菜或副菜。
此外,也因为主菜 (被装饰者)、副菜 (装饰者) 都是继承自 Meal 这个最顶层的基类,所以客户端程序 (Page_Load) 在引用他们的对象时,把 new 出来的主菜实例、new 出来的副菜实例,再抛入其他副菜实例的构造函数中,也会相容、不会发生错误,在相互引用和「运行」时期的「动态」扩展上,变得很有弹性。
最后补充一点,Decorator 模式中,装饰者类对象,在组合时的「顺序」亦很重要。本帖图 1 中,ConcreteDecoratorA、ConcreteDecoratorB、ConcreteDecoratorC、... 等具体装饰者,在动态添加和组合时,在某些实际应用中必须依照一定的顺序,若顺序相反可能会导致程序执行结果不合理,但本帖所提供的几个例子中并不需要考虑到这点。
--------------------------------------------------------
Decorator Pattern 适用的情景:
- 你拥有一个已存在的组件类,却无法继承它 (subclassing)。
- 能够动态地为对象添加职责 (添加状态和行为)。
- 改变类中的成员和行为,但不影响其他对象。
- 希望能便于职责的撤消。
- 不想用「继承」来扩展行为。其中一种原因是避免当一些功能要交叉搭配引用时,单独用「继承」来设计会产生太多的子类、太复杂的类图结构 [1], [10];另一种考量可能是因为类的定义被隐藏,或类的定义不能用于生成子类。
Decorator Pattern 的优点:
- 可避免单独使用「继承」时,在扩展时不够弹性,且可能衍生过多的子类。
- 扩展时不需要修改既有的代码。
- 可在执行时期,动态地添加新行为 (职责)。
Decorator Pattern 的缺点:
- 可能会在程序中出现许多的小型类,亦即需要编写很多 ConcreteDecorator 类 (具体装饰者)。
- 若过度使用 Decorator 模式,会让程序逻辑变得很复杂。
- 别人较不易理解设计方式及代码,排查故障、追踪和调试也比较困难。
Decorator Pattern 的其他特性:
- 每个要装饰的功能,都放在单独的类中。
- 我们可以用无数个装饰者,去包装一个组件。
- 「装饰者」可以在「被装饰者」前面或后面,添加自己的行为,甚至将「被装饰者」的行为整个取代掉,以达到特定的目的。
- 「被装饰者」并不需要知道它已经被「装饰」过了,亦即 Component 类 (对象) 并不需要知道 Decorator 类 (对象) 的存在,且 Decorator 也仅仅认识 Component。
--------------------------------------------------------
最后再补充,在 Java I/O、.NET I/O、.NET System.Windows.Controls 命名空间中,很多地方都有用到 Decorator 模式来设计 [2], [5], [12]。例如我们查询 MSDN Library,会发现 .NET 3.x 版本的 Windows Forms 其 API 里,甚至还有增加一个新的 Decorator 类,如下:
System.Object
System.Windows.Threading.DispatcherObject
System.Windows.DependencyObject
System.Windows.Media.Visual
System.Windows.UIElement
System.Windows.FrameworkElement
System.Windows.Controls.Decorator
System.Windows.Controls.Viewbox
System.Windows.Controls.Decorator 类:
提供在单个子元素(如 Border 或 Viewbox)上或周围应用效果的元素的基类。
http://msdn.microsoft.com/zh-cn/library/system.windows.controls.decorator.aspx
http://msdn.microsoft.com/zh-cn/library/system.windows.controls.decorator(VS.85).aspx
以下列出 Decorator Pattern,用于实际程序的四种方式 [12]:
- 装饰模式非常适合于「图形程序」,亦适合于「视频和声音」,比如:视频流可用不同的比率进行压缩,而声音可以输入给同声传译服务。
- 装饰模式如此有用,以致于 .NET 3.0 中,已经有实际的 Decorator 类了。如刚才提到的,在 System.Windows.Controls 命名空间中,提供了一些基类,可用于给其他的图形元素添加特殊效果,如:Border 类和 Viewbox 类。
- 在如今的移动设备上,Web 浏览器及其他应用中,也大量运用 Decorator Pattern。比如这些设备上可以创建适合小屏幕显示的对象,跟标准桌面机上的浏览器相比,这些屏幕可能也含有滚动条,但不含有标题栏 [6]。
- 从更一般的层次上,.NET 的 I/O API 中,到处都是装饰模式 [6],如下方的体系:
System.Object
System.MarshalByRefObject
System.IO.Stream
System.IO.BufferedStream
System.IO.FileStream
System.IO.MemoryStream
System.Net.Sockets.NetworkStream
System.Security.Cryptography.CryptoStream
上方体系的最后五个子类,有的就装饰了 Steram 这个父类 (如:BufferedStream 类),因为它们从它继承,同时又含有 Stream 类的实例,而且这个实例是子类被构造时传递进去的 (如同本帖第二个西餐店的示例中,副菜的构造函数的写法)。如下方 MSDN Library 代码的 BufferedStream 类,其构造函数的第一个参数,即为其父类 System.IO.Stream 的变量 stream。
BufferedStream 构造函数:
public BufferedStream(
Stream stream
)
http://msdn.microsoft.com/zh-cn/library/system.io.bufferedstream.bufferedstream(VS.80).aspx
--------------------------------------------------------
本帖的最后,提供一位 Java 大师 - 结城浩,所绘制的 Decorator Pattern 趣味四格漫画 [9],原地址如下:
Giko 猫谈 DP 四格漫画:
http://www.hyuki.com/dp/cat_Decorator.html
http://www.javaworld.com.tw/jute/post/view?bid=44&id=40923&sty=3
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
∧_∧ 敲敲敲 ╱
( ) ∧ ∧ < System.IO.Stream 是个典型的例子..恩....。
( ) (,,゚Д゚) ╲____________
______ (つ_つ____
| 日∇ ╲|ThinkPad|╲
| ========= ╲
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
____________
╱
| 喔~、是 Decorator Pattern 吗?
╲ __ __________
|╱
∧_∧ ╱
( ・∀・) ∧ ∧ < 你你是谁? ...有..有什么事嬷你?・・・
( ⊃ ) (゚Д゚;) ╲____________
________(つ_つ____
| 日∇ ╲|ThinkPad|╲
| ========= ╲
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
__________
╱
| 将 Concrete Decorator 重迭起来达到想要的目的..恩
╲ __ ________
|╱
∧_∧ ╱
( ・∀・) ∧ ∧ < 恩...恩恩..没错....
( ) (;゚Д゚) ╲____________
_____ (つ_つ____
| 日∇╲|ThinkPad|╲
| ========= ╲
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
__________
╱
| 乱做一堆细微的类的人就是你.恩.
╲ __ ________
|╱
∧_∧ ╱
( ・∀・) ∧ ∧ < 不..不是我..!!
( ⊃ ) (゚Д゚;) ╲____________
_____(つ_つ____
| 日∇ ╲|ThinkPad|╲
| ========= ╲
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -