《Head.First设计模式》的学习笔记(4)--装饰者模式

意图:动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

结构

image

例子

下面我们以星巴兹(Starbuzz)的订单系统为例加以说明。

需求分析:

1)、星巴兹的饮料(Beverage)种类繁多,主要有HouseBlend、DarkRoast、Decaf、Espresso。

2)、星巴兹的调料很多,主要有Steamed Milk、Soy、Mocha、Whip。

3)、星巴兹的饮料价格是根据饮料的基础价和所加入的调料的价格相加得到。

错误设计:

根据以上的简单分析,第一种类图设计出炉:

image

其中getDescription()用来描述饮料,cost()用来计算价格。

显而易见,这个类图设计的最大缺点就是类太多,系统难以维护。所以我们需要另外的解决方案,而且新方案必须避免“类爆炸”。此时我们想到了实例变量和继承。先从Beverage基类下手,加上实例变量代表是否加上调料(Steamed Milk、Soy、Mocha、Whip等),Beverage基类的cost()计算调料的价钱,而各种具体的饮料(HouseBlend、DarkRoast、Decaf、Espresso等)的cost()将把基础饮料的价钱和调料的价钱相加得到饮料的价钱。由此可以设计出第二种类图

image

对这个类图设计的评价:如果需求不再变化,那么这个类图设计没有错;但是需求发生了变化,这个设计就会难以招架。经过进一步的分析,我们发现部分需求被我们遗漏了。

新增加的需求:

1)、调料的价格可能发生变化。

2)、调料的种类可能发生变化。

3)、饮料的种类可能增加,不只HouseBlend、DarkRoast、Decaf、Espresso四种。

4)、顾客可能在一种饮料里加双份的同种饮料。

显然,第二种类图设计难以满足新的需求,而且对新增加的饮料而言,有可能存在不适合的调料。例如,茶子类将继承hasWhip()(加奶泡)等方法。因此,我们应该对类图设计进行改进。此时我们想到了装饰者模式,那么如何应用装饰者模式呢?

以装饰者模式构造饮料订单:

1)、以DarkRoast对象开始。image

2)、顾客想要Mocha,所以建立一个Mocha对象,并用它将DarkRoast对象包装起来。image

3)、顾客也想要Whip,所以需要建立一个Whip装饰者,并用它将Mocha对象包起来。image

4)、现在,该是为顾客算钱的时候了。通过最外圈装饰者(Whip)的cost()就可以办得到。Whip的cost()会先委托它装饰的对象(Mocha)计算出价钱,然后再加上Whip的价钱。

image

根据以上的分析,应用装饰者模式,我们可以打造一个全新的类图。

第三种类图设计(正确的类图):

image

部分代码为:

 1public class DarkRoast : Beverage
 2    {
 3        public DarkRoast()
 4        {
 5            description = "Dark Roast";
 6        }
    
 7        public override double Cost()
 8        {
 9            return 1.22;
10        }

11    }

12public class Mocha : CondimentDecorator
13    {
14        Beverage myBeverage;
15        public Mocha(Beverage paramBeverage)
16        {
17            this.myBeverage = paramBeverage;
18        }

19
20        public override string GetDescription()
21        {
22            return myBeverage.GetDescription() + ",Mocha";
23        }
 
24    
25        public override double Cost()
26        {
27            return 0.5 + myBeverage.Cost();
28        }

29    }

30class StarbuzzCoffee
31    {
32        static void Main(string[] args)
33        {
34            Beverage myBeverage = new Espresso();
35            Console.WriteLine(myBeverage.GetDescription() + " $" + myBeverage.Cost());
36
37            Beverage myBeverage2 = new DarkRoast();
38            myBeverage2 = new Mocha(myBeverage2);
39            myBeverage2 = new Mocha(myBeverage2);
40            myBeverage2 = new Whip(myBeverage2);
41            Console.WriteLine(myBeverage2.GetDescription() + " $" + myBeverage2.Cost());
42
43            Console.ReadLine();
44        }

45    }

  

 装饰模式的适用情况:

1)、需要扩展一个类的功能,或给一个类增加附加责任。

2)、需要动态地给一个对象增加功能,这些功能可以再动态地撤销。

3)、需要增加由一些基本功能的排列组合而产生的非常大量的功能,从而使继承关系变得不现实。

使用装饰模式主要有以下的优点:

1)、装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。

2)、通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。

使用装饰模式主要有以下的缺点:

由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是,在另一方面,使用装饰模式会产生比使用继承关系更多的对象。更多的对象会使得查错变得困难,特别是这些对象看上去都很相像。

参考文献: 

《Head.First设计模式》

  吕震宇  设计模式系列

源代码下载:/Files/wxj1020/StarbuzzCoffee.rar

Tag标签: 设计模式
posted @ 2008-03-31 08:31 鹰击长空 阅读(2331) 评论(18)  编辑 收藏 所属分类: 设计模式

  回复  引用  查看    
#1楼 2008-03-31 09:05 | 狼Robot      
楼主的书是不是PDF的,是不是影印版的.我下了个,虽然看得清,但看起很不舒服.
  回复  引用  查看    
#2楼 2008-03-31 09:47 | Tony Zhou      
呵呵,我也在看,我看到组合模式了。
  回复  引用    
#3楼 2008-03-31 10:53 | 盒饭 [未注册用户]
呵呵。不错,支持一下。
  回复  引用  查看    
#4楼 2008-03-31 11:47 | 在现实和理想之间行走      
这个问题这样设计不太合适吧?星巴兹的饮料种类不仅多,而且会随着业务的发展不断有新的饮料开发出来,把这样的一个产品设计为一个类的话,那如果有新产品出来的话,难道你要到客户那里去添加一个类重新部署吗?
  回复  引用  查看    
#5楼 2008-03-31 11:51 | 在现实和理想之间行走      
这个例子倒像是专门设计来解释装饰模式的,实战环境中应该不会出现这样的设计吧
  回复  引用  查看    
#6楼 [楼主]2008-03-31 12:04 | 长空新雁      
@狼Robot
跟你的一样。
◎Tony Zhou
一起学习,共同进步
◎盒饭
谢谢支持
  回复  引用  查看    
#7楼 [楼主]2008-03-31 12:12 | 长空新雁      
◎在现实和理想之间行走
你说的很对,这些例子主要是为了讲解设计模式,设计的比较简单,但原理是不会变的。实战环境中可能会通过Ioc容器或者配置文件等来配置产品类。
  回复  引用  查看    
#8楼 2008-03-31 12:21 | 在现实和理想之间行走      
Ioc容器并不能解决生成一个系统中不存在的类这个事情,它解决的只是依赖倒置,针对系统中已经存在的类的,所以我觉得Ioc容器不是扩展的办法的
  回复  引用  查看    
#9楼 [楼主]2008-03-31 12:31 | 长空新雁      
◎在现实和理想之间行走
如果有新产品出来的话,运用配置文件和反射技术是完全可以做到的,不需要重新部署,只需要修改配置文件就行了。
希望你能把你的解决方案写出来,好让我们共同学习。
  回复  引用  查看    
#10楼 2008-03-31 13:30 | songcan      
运用配置文件和反射技术是完全可以做到的,不需要重新部署,只需要修改配置文件就行了。

恕我愚钝,配置文件和反射技术能解决生成一个系统中不存在的类这个事情?貌似不可以把,不更新DLL,类如何添加上去?

--引用--------------------------------------------------
在现实和理想之间行走: 这个问题这样设计不太合适吧?星巴兹的饮料种类不仅多,而且会随着业务的发展不断有新的饮料开发出来,把这样的一个产品设计为一个类的话,那如果有新产品出来的话,难道你要到客户那里去添加一个类重新部署吗?
--------------------------------------------------------
我不知道这有何不妥,请指教你的不用添加新类的方法,谢谢!

  回复  引用  查看    
#11楼 2008-03-31 13:31 | songcan      
设计原则,对修改关闭,对扩展开放,我想添加一个新的扩展类并不违反这个原则吧
  回复  引用  查看    
#12楼 [楼主]2008-03-31 14:22 | 长空新雁      
@songcan
dll肯定需要更新。
这个设计的不妥主要体现在Beverage myBeverage = new Espresso();
等语句中(其实这样写只是为了方便测试)。
  回复  引用  查看    
#13楼 2008-03-31 14:37 | 坚强2002      
@songcan
当你写了一个HttpModule 的时候你是怎么做的呢?你把这个dll添加到Bin目录,然后在Web.config添加了一下配置,这个过程是不是可以帮助你理解呢?

  回复  引用  查看    
#14楼 2008-03-31 14:45 | 阿不      
装饰模式重点在于通过组合一些相同接口的类,(这些相同接口的类很多都是实现描述特定功能的类)来实现动态的功能组合。它的很大的灵活在于用户可以方便的通过自己的组合,来得到功能不同(或功能强化)的对象。而如果对于系统内部希望产生一些功能可变,而又希望这种变化不通过修改代码而来,那么是不是可以考虑通过配置和IOC的方式来实现。
  回复  引用  查看    
#15楼 [楼主]2008-03-31 14:53 | 长空新雁      
@坚强2002
@阿不
谢谢高手的精辟讲解。
  回复  引用  查看    
#16楼 2008-03-31 15:18 | 炭炭      
整这么复杂干吗?让我卖糖葫芦的给你解释下什么叫装饰模式:
我有山楂、橘子、苹果 3种水果
我以前是这么卖的:预先把糖葫芦都插好,比如山楂的1个,橘子的2个,苹果的5个,或者山楂的5个,橘子的2个,苹果的1个,然后拿去卖。如果买的人有正好他中意的,他就买。可是还有一些情况是,他要买山楂的2个,橘子的2个,苹果的5个而我没有插这种,那么就没办法,我只有答应下次给他插。但是买的人口味是变化的,我渐渐发现我需要覆盖的组合越来越多,而能卖出去却很少。痛苦!
现在我学会了,把山楂、橘子、苹果 3种水果直接拿到市场上去,买的人需要什么我就当时给他组合。当然这些山楂、橘子、苹果 3种水果并不是原来的样子了,它们每一块我都通了一个孔,这样查起来就方便多了。这个孔就是LZ第一个类图的component。还有每个糖葫芦最上面那颗是略微有所不同的,它只有一个孔,那就是concretcomponent了。
呵呵,大道相通,不过如此。

标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
 
另存  打印