装饰者模式

装饰者模式

       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是装饰者模式里非常特殊的一个类,它既继承于ComponentIS A关系】,又维护一个指向Component实例的引用【HAS A关系】,换个角度来说,DecoratorComponent之间,既有动态组合关系又有静态继承关系,WHY? 这里为什么要这么来设计?上面我们说过,组合的好处是可以在运行时给对象增加职责,DecoratorHAS AComponent的目的是让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

ExpressoDarkRoastHouseBlendDecaf


 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,内部实现很简单,代码结构也几乎都一样,只是具体的返回值不同,也没什好说的,看代码吧。

SoySteamedMilkWhipMocha


 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调味品,最后获取咖啡的总价和描述的时候,都会自动包含调味品的部分,我们的目的达到了!

 

   完整代码下载:(VS2005NUnit2.2/2.4)

应用场景和优缺点

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

    装饰者模式的应用场景:

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

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

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

装饰者模式的优点:

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

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

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

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

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

装饰者模式的缺点:

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

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值