设计模式之Decorator(装饰者模式)

本文深入讲解装饰者模式,探讨如何动态地给对象增加新的职责,同时保持代码的高度灵活性和扩展性。通过具体案例分析,展示装饰者模式在实际项目中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这篇文章代码还有一些实际的内容不知道如何动手,

还是把网上看到一篇与我书看的head first 设计模式一书中一样的例子放上来吧。

 

来杯咖啡

       Central Perk 的名字因为《老友记》而享誉全球,他们的分店几乎开遍世界各地。他们发展的实在是太快了,所以他们此时正在急于实现一套由计算机管理的自动化记账系统。在第一次研究了他们的需求以后,开发者设计了如下图的类结构:

 

       Beverage 是所有饮料的基类; cost() 是抽象方法,所有子类都需要定义它们自己的 cost() 实现来返回特定饮料的价钱; description 变量也是在子类里赋值的,表示特定饮料的描述信息, getDescription() 方法可以返回这个描述;   

       除了咖啡以为, Central Perk 还提供丰富的调味品,比如:炼乳、巧克力、砂糖、牛奶等,而且这些调味品也是要单独按份收费的,所以调味品也是订单系统中重要的一部分。

       于是,考虑到调味品的管理,开发者又有了下面这样的类结构:

       看了上面的类图,你一定有话要说!是的!这简直是太恐怖了,好像是整个类图都要爆炸了一样,而且以后随便增加一种调味品,继承于 Beverage 的子类还会翻倍! ( 因为理论上可能的咖啡种类数 = 咖啡类别数×调味品类别数 ) 我的神啊!全球变暖后连咖啡都“沸腾”了吗?还是我们在研制某种自杀式炸弹装置! ( 方便携带,不宜察觉,居家旅游必备,只要将几种调味品混合到一起,就能产生惊人的爆炸力!不错啊! J )   

       上面的情况绝对是不能容忍的,于是开发者们经过讨论,又提出了下面的设计方案件:

       如图所示,这是改进后的 Beverage 基类。首先在基类里增加了表示是否包含特定调味品的布尔变量,如 milk , soy 等,然后提供了一些 has(get) set 方法来设置这些布尔值;其次在 Beverage 类里实现 cost() 方法来计算调味品的价钱。所有咖啡子类将仍然覆盖 cost() 方法,只是这次它们需要同时调用基类的 cost() 方法,以便获得咖啡加上调味品后的总价。   

       看上去似乎这是一个不错的设计,那么下面我们再来给 Beverage 增加子类,如下图所示:

      

 

       基类的 cost() 方法将计算所有调味品的价钱 ( 当然是只包括布尔值为 true 的调味品 ) ,子类里的 cost() 方法将扩展其功能,以包含特定类型饮料的价钱。

       OK! 现在我们似乎已经有了一个看上去还不错的设计,那么 Central Perk 的这个记账系统就按这个设计来实现就万事大吉了吗?等一下,还是让我们先从以前学习过的“找到系统中变化的部分,将变化的部分同其它稳定的部分隔开。 ”这个设计原则出发,重新推敲一下这个设计。

那么对于一家咖啡店来说,都有那些变化点呢?调味品的品种和价格会变吗?咖啡的品种和价格会变吗?咖啡和调味品的组合方式会变吗? YES! 对于一家咖啡店来说,这些方面肯定会经常发生改变的!那么,当这些改变发生的时候,我们的记账系统要如何应对呢? 如果调味品发生改变,那么我们只能从代码的层次重新调整 Beverage 基类,这太糟糕了;如果咖啡发生改变,我们可以增加或删除一个子类即可,这个似乎还可以忍受;那么咖啡和调味品的组合方式发生改变呢?如果顾客点了一杯纯黑咖啡外加两份砂糖和一份巧克力,或者顾客点了一杯脱咖啡因咖啡 ( Decaf ) 外加三份炼乳和一份砂糖呢?我倒!突然意识到,上面的设计根本不支持组合一份以上同种调味品的情况,因为基类里的布尔值只能记录是否包含某种调味品,而并不能表示包含几份,连基本的功能需求都没有满足,看来这些开发者可以卷铺盖滚蛋了! ( 似乎他们改行去做炸弹更合适! )

       好吧!让我们来接手这个设计!我们已经分析了前面设计的失败之处,我们应该实现支持调味品的品种和价格任意改变而不需要修改已有代码的设计;我们还要实现支持咖啡品种和价格任意改变而不需要修改已有代码的设计 ( 这点上面的设计通过继承算是实现了 ) ;还有就是支持咖啡和调味品的品种和份数任意组合而不需要修改已有代码的设计;还有就是代码重用越多越好了,内聚越高越好了,耦合越低越好了; ( 还有最重要的,报酬越高越好啦! )

       看来我们要实现的目标还真不少,那么我们到底该怎么做呢?说实话,我现在也不知道!我们需要先去拜访一下今天的主角—装饰者模式,看看她能给我们带来什么惊喜吧!

这就是装饰者模式

我们还是先看一下官方的定义:

The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. ( 装饰者模式可以动态地给一个对象增加其他职责。就扩展对象功能来说,装饰者模式比生成子类更为灵活。 )

这里我们要重点注意那个 dynamically (动态的),什么是动态?静态又是什么?这是我们要重点区分的地方,后面我们还会专门讨论这个问题。下面先看看装饰者模式的类图和顺序图:

Component (被装饰对象基类)   

l         定义对象的接口,可以给这些对象动态增加职责;

ConcreteComponent (具体被装饰对象)

l         定义具体的对象, Decorator 可以给它增加额外的职责;

Decorator (装饰者抽象类)

l         维护一个指向 Component 实例的引用 ,并且定义了与 Component 一致的接口;

ConcreteDecorator (具体装饰者)

l         具体的装饰对象,给内部持有的具体被装饰对象增加具体的职责;

       怎么样?大家都看懂了吧 ! 看懂了我就不解释了!— 呵呵,开玩笑的!

我们先来说说上面提到的动态和静态的问题,所谓动态是说可以在系统运行时 (RunTime) 动态给对象增加其它职责而不需要修改代码或重新编译;所谓静态是说必须通过调整代码 (DesignTime) 才能给对象增加职责 , 而且系统还需要重新编译;从具体技术层面来说,对象的组合和继承正好对应于前面的动态和静态,因为通过对象组合建立的交互关系不是在代码中 (DesignTime) 固定死的,而是在运行时 (RunTime) 动态组合的;而通过继承建立的关系是僵硬的难以改变的,因为它是在代码中 (DesignTime) 固定死了的,根本不存在运行时 (RunTime) 改变的可能。换个角度说:我们应该多使用对象组合来保持系统的运行时扩展性,尽量少使继承,因为继承让程序变得僵硬!这句话听着是不是很熟悉啊?恩!这就是我们前面文章里提过多次的一个设计原则: Favor composition over inheritance. (优先使用对象组合,而非类继承),更多的就不需要再解释了吧?

       那么回到装饰者模式,跟前面介绍过的模式一样,装饰者同样是一个很简单的模式,特别是画出类图和顺序图之后,一切都很清楚明了。这里只有一个地方需要特殊强调一下: 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 】,还要做人该做的事情,但是可能会对异性更有吸引力了 ( 扩展功能 )     

       现 在我们已经认识了装饰者模式,知道了动态关系和静态关系是怎么回事,是时候该解决咖啡店的问题了,从装饰者模式的角度来考虑问题,咖啡和调味品的关系应该 是:咖啡是被装饰对象而调味品是装饰者,咖啡和调味品可以任意组合,但是不管怎么组合,咖啡还是咖啡!原来这么简单啊!具体看下面的类图:

       如图所示, Beverage 还是所有饮料的基类,它对应于装饰者模式类图里的 Component , 是所有被装饰对象的基类; HouseBlend , DarkRoast , Espresso , Decaf 是具体的饮料 ( 咖啡 ) 种类,对应于前面的 ConcreteComponent ,即是具体的被装饰对象; CondimentDecorator 对应于前面的 Decorator ,是装饰者的抽象类;而 Milk Mocha Soy Whip 则都是具体的调味品,对于前面的 ConcreteDecorator ,也就是具体的装饰者。下面我们通过具体的代码再进一步理解一下基于装饰者模式的记账系统的实现。   

       Beverage

 1 using  System;
 2
 3 namespace  DesignPatterns.Decorator.CentralPerk
 4 {
 5      public   abstract   class  Beverage
 6      {
 7          private  BeverageSize size;
 8
 9          public   virtual   string  GetDescription()
10          {
11              return   " Unknown Beverage " ;
12         }

13
14          public  BeverageSize Size
15          {
16              get
17              {
18                  return  size;
19             }

20              set
21              {
22                 size  =  value;
23             }

24         }

25
26          public   abstract   double  Cost();
27     }

28 }

29

关于这段代码,似乎没什么好说的了,因为上面已经反复说了很多啦。

       CondimentDecorator


 1 using  System;
 2
 3 namespace  DesignPatterns.Decorator.CentralPerk
 4 {
 5      public   abstract   class  CondimentDecorator : Beverage
 6      {
 7          public  CondimentDecorator()
 8          {}
 9     }

10 }

11
 

这只是一个最简单的抽象类,目的只是为了让所有装饰者跟被装饰者统一都继承于 Beverage

Expresso DarkRoastHouseBlendDecaf


 1 using  System;
 2 using  System.Configuration;
 3
 4 namespace  DesignPatterns.Decorator.CentralPerk
 5 {
 6      public   class  Expresso: Beverage
 7      {
 8          public  Expresso()
 9          {}
10
11          public   override   double  Cost()
12          {
13              return  GetSize( base .Size);
14         }

15
16          public   override   string  GetDescription()
17          {
18              return   " Expresso " ;
19         }

20
21          private   double  GetSize(BeverageSize size)
22          {
23              switch (size)
24              {
25                  case  BeverageSize.TALL:
26                      return   1.50 ;
27                  case  BeverageSize.GRANDE:
28                      return   1.65 ;
29                  case  BeverageSize.VENTI:
30                      return   1.75 ;
31                  default :
32                      return   1.50 ;
33             }

34         }

35
36     }

37 }

38

 

 1 using  System;
 2
 3 namespace  DesignPatterns.Decorator.CentralPerk
 4 {
 5      public   class  DarkRoast : Beverage
 6      {
 7          public  DarkRoast()
 8          {}
 9
10          public   override   double  Cost()
11          {
12              return  . 99 ;
13         }

14
15          public   override   string  GetDescription()
16          {
17              return   " Dark Roast Coffee " ;
18         }

19
20     }

21 }

22

 

 1 using  System;
 2
 3 namespace  DesignPatterns.Decorator.CentralPerk
 4 {
 5      public   class  HouseBlend : Beverage
 6      {
 7          public  HouseBlend()
 8          {}
 9
10          public   override   double  Cost()
11          {
12              return  . 89 ;
13         }

14
15          public   override   string  GetDescription()
16          {
17              return   " House Blend Coffee " ;
18         }

19
20     }

21 }

22

 

 1 using  System;
 2
 3 namespace  DesignPatterns.Decorator.CentralPerk
 4 {
 5      public   class  Decaf : Beverage
 6      {
 7          public  Decaf()
 8          {}
 9
10          public   override   double  Cost()
11          {
12              return   1.05 ;
13         }

14
15          public   override   string  GetDescription()
16          {
17              return   " Decaf Coffee " ;
18         }

19
20     }

21 }

22

 

这几个类都是具体的饮料 ( 咖啡 ) 了,也就是最后要被调味品装饰的对象,他们都继承于 Beverage ,内部实现很简单,代码结构也几乎都一样,只是具体的返回值不同,也没什好说的,看代码吧。

Soy SteamedMilkWhipMocha


 1 using  System;
 2
 3 namespace  DesignPatterns.Decorator.CentralPerk
 4 {
 5      public   class  Soy : CondimentDecorator
 6      {
 7         Beverage beverage;
 8         
 9          public  Soy(Beverage beverage)
10          {
11              this .beverage  =  beverage;
12         }

13
14          public   override   string  GetDescription()
15          {
16              return  beverage.GetDescription()  +   " , Soy " ;
17         }

18
19          public   override   double  Cost()
20          {
21              return  . 15   +  beverage.Cost();
22         }

23     }

24 }

25

 

 1 using  System;
 2
 3 namespace  DesignPatterns.Decorator.CentralPerk
 4 {
 5      public   class  SteamedMilk : CondimentDecorator
 6      {
 7         Beverage beverage;
 8         
 9          public  SteamedMilk(Beverage beverage)
10          {
11              this .beverage  =  beverage;
12         }

13
14          public   override   string  GetDescription()
15          {
16              return  beverage.GetDescription()  +   " , Steamed Milk " ;
17         }

18
19          public   override   double  Cost()
20          {
21              return  . 10   +  beverage.Cost();
22         }

23     }

24 }

25

 

 1 using  System;
 2
 3 namespace  DesignPatterns.Decorator.CentralPerk
 4 {
 5      public   class  Whip : CondimentDecorator
 6      {
 7         Beverage beverage;
 8         
 9          public  Whip(Beverage beverage)
10          {
11              this .beverage  =  beverage;
12         }

13
14          public   override   string  GetDescription()
15          {
16              return  beverage.GetDescription()  +   " , Whip " ;
17         }

18
19          public   override   double  Cost()
20          {
21              return  . 10   +  beverage.Cost();
22         }

23     }

24 }

25

 

 1 using  System;
 2 using  System.Configuration;
 3
 4 namespace  DesignPatterns.Decorator.CentralPerk
 5 {
 6      public   class  Mocha : CondimentDecorator
 7      {
 8         Beverage beverage;
 9         
10          public  Mocha(Beverage beverage)
11          {
12              this .beverage  =  beverage;
13         }

14
15          public   override   string  GetDescription()
16          {
17              return  beverage.GetDescription()  +   " , Mocha " ;
18         }

19
20          public   override   double  Cost()
21          {
22              return  GetSize( base .Size);
23         }

24
25          private   double  GetSize(BeverageSize size)
26          {
27              switch (size)
28              {
29                  case  BeverageSize.TALL:
30                      return  . 20   +  beverage.Cost();
31                  case  BeverageSize.GRANDE:
32                      return  . 25   +  beverage.Cost();
33                  case  BeverageSize.VENTI:
34                      return  . 30   +  beverage.Cost();
35                  default :
36                      return  . 20 ;
37             }

38         }

39
40     }

41 }

42

 

这几个类都是具体的调味品了,也就是装饰者,他们都继承于,内部代码结构同样是几乎都一样,关键点是他们内部都有一个 Beverage 类型的变量,是在构造函数里初始化的,这个变量就是它们要装饰的对象,在价钱和描述上会扩展不同的实现。

上面只是具体的实现代码,并没有具体结果的演示,于是这里提供了一个基于NUnit 的测试项目,测试的同时也是很好的演示代码,这里只做简单介绍,具体大家看代码便知。

DecoratorStarbuzzFixture


 1 using  System;
 2 using  DesignPatterns.Decorator.CentralPerk;
 3 using  NUnit.Framework;
 4
 5 namespace  Test.DesignPatterns.Decorator.CentralPerk
 6 {
 7     [TestFixture]
 8      public   class  DecoratorStarbuzzFixture
 9      {
10          TestExpresso // TestExpresso
19
20          TestExpressoWithSize // TestExpressoWithSize
33
34          TestHouseBlend // TestHouseBlend
45
46          TestDarkRoast // TestDarkRoast
57
58          TestDecaf // TestDecaf
69     }

70 }

71

 

TestDarkRoast 为例,代码里先初始化了一个咖啡 ( 饮料 ) 对象实例,然后给它装饰 ( 添加 ) 一份 Mocha 调味品,再装饰 ( 添加 ) 一份 Soy 调味品 , 最后获取咖啡的总价和描述的时候,都会自动包含调味品的部分,我们的目的达到了!

 

 

 

应用场景和优缺点

上面已经对装饰者模式做了比较详细的介绍,还是那句话,人无完人,模式也不是万能的,我们要用好设计模式来解决我们的实际问题,就必须熟知模式的应用场景和优缺点:

    装饰者模式的应用场景:

1、  想透明并且动态地给对象增加新的职责的时候。

2、  给对象增加的职责,在未来存在增加或减少可能。

3、  用继承扩展功能不太现实的情况下,应该考虑用组合的方式。

装饰者模式的优点:

1、  通过组合而非继承的方式,实现了动态扩展对象的功能的能力。

2、  有效避免了使用继承的方式扩展对象功能而带来的灵活性差,子类无限制扩张的问题。

3、  充分利用了继承和组合的长处和短处,在灵活性和扩展性之间找到完美的平衡点。

4、  装饰者和被装饰者之间虽然都是同一类型,但是它们彼此是完全独立并可以各自独立任意改变的。

5、  遵守大部分 GRASP 原则和常用设计原则,高内聚、低偶合。

装饰者模式的缺点:

1、  装饰链不能过长,否则会影响效率。

2、  因为所有对象都是 Component, 所以如果 Component 内部结构发生改变,则不可避免地影响所有子类 ( 装饰者和被装饰者 ) ,也就是说,通过继承建立的关系总是脆弱地,如果基类改变,势必影响对象的内部,而通过组合 (Decoator HAS A Component) 建立的关系只会影响被装饰对象的外部特征。

3 、只在必要的时候使用装饰者模式,否则会提高程序的复杂性,增加系统维护难度。

    【备注:关于场景和优缺点,上面肯定说得不够全面,欢迎大家来补充。】

.NET 框架里的应用

        装饰者模式在 .NET 框架里的应用也很多,这里举个非常有代表性的例子,更多的实例还要大家自己在实践中细心多观察。 WinForm 里有一个很有意思的控件: ToolTip, 简单地说这个控件的作用就是可以为 Form 上的其它控件动态增加 ToolTip 功能。

如图一所示,没有添加 ToolTip 控件前, Button 控件的属性中并没有实现 ToolTip 的属性,即 Button 控件本身没有 ToolTip 这个功能;   

如图二所示,在添加了 ToolTip 控件以后,在 Button 的属性里自动多出了“ ToolTip1 上的 ToolTip ”这项,它的作用就是可以为 Button 自动增加 ToolTip 的功能,例如这里我输入“这是一个按钮!”,则在程序执行后,如图三所示:   

当鼠标移动到 button1 上的时候,就会自动弹出刚才设置的 Tip 消息框!是不是很酷啊! Button 控件神奇地动态获得了 ToolTip 的功能,这个过程甚至 Button 控件根本什么都不知道,这还了得,这样下去岂不是可以扩展出各种各样的功能了! Yeah! 这就是 Decorator 厉害的地方,它赋予了我们给对象动态扩展功能的能力 , 至于扩展出什么功能,那就看具体的实现了。   

因为上面的实例代码非常简单,这里就不提供下载了。

相关设计原则

相信大家现在对装饰者模式都应该很清楚了吧!那么,就像我们在前面的文章里反复强调的一样,设计原则远比模式重要,学习设计模式的同时一定要注意体会设计原则的应用。这里我们再来看看装饰者模式里都符合那些主要的设计原则。

1、  Identify the aspects of your application that vary and separate them from what stays the same. ( 找到系统中变化的部分,将变化的部分同其它稳定的部分隔开。 )

在装饰者模式的应用场景里变化的部分是 Component 的扩展功能。使用 Decorator 模式可以很好地将装饰者同被装饰者完全隔离开,我们可以任意改变 ConcreteComponent ConcreteDecorator ,它们之间不会有任何相互影响。

2、  Program to an interface,not an implementation. (面向接口编程,而不要面向实现编程。)

Component Decorator 都是抽象类实现,其实相当于起到很好的接口隔离作用,在运行时具体操作的也是 Component 类型的变量引用,这完全是面向接口编程的。

3、  Favor composition over inheritance. (优先使用对象组合,而非类继承)

装饰者模式最成功的地方就是合理地使用了对象组合,通过组合灵活地扩展了 Component 的功能,所有的扩展的功能都是通过组合而非继承获得的,这从根本上决定了这种实现是高内聚低耦合的。

4、  Classes should be open for extension, but closed for modification ( 类应该对扩展开发,对修改关闭 )

这是大名鼎鼎的 OCP 原则,我们在这个系列的第一篇【模式和原则 】里就有专门的介绍。在装饰者模式里充分体现了 OCP 原则,在需要扩展 Component 的功能的时候,只需要实现一个新的特定的 ConcreteDecorator 即可,这完全是一种增量开发,不会对原来代码造成任何影响,对用户代码完全是透明的。

Code should be closed (to change) like the lotus flower in the evening, yet open (to extension) like the lotus flower in the morning. From HFDP

BULLET POINTS

l        Inheritance is one form of extension, but not necessarily the best way to achieve flexibility in our designs.

l        In our designs we should allow behavior to be extended without the need to modify existing code.

l        Composition and delegation can often be used to add new behaviors at runtime.

l        The Decorator Pattern provides an alternative to subclassing for extending behavior.

l        Decorator Classes mirror the type of the Components they decorate. (In fact, they are the same type as the components they decorate, either through inheritance or interface implementation.)

l        Decorators change the behavior of their components by adding new functionality before and/or after (or even in place of) method calls to the component.

l        You can wrap a component with any number of decorators.

l        Decorators are typically transparent to the client of the component; that is,unless the client is relying on the component's concrete type.

l        Decorators can result in many small objects in our design, an overuse can be complex.

 

 

 

作者:Justin
出处:http://justinw.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利

 

 

 

看本设计模式系列也可以参考一些资源

如本例子中讲的就结合了这本书里的内容

书名叫:

Head First 设计模式(中文版)

我的下载里已经上传上去了

http://download.youkuaiyun.com/xupeihuagudulei

 

可能目前还看不了。

 

祝大家学习愉快!


 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值