装饰者模式
1、我曾经以为男子汉应该用继承处理一切,后来我领教到运行时扩展,远比编译时期的继承威力大,看看我现在光彩的样子
2、“给爱用继承的人一个全新的设计眼界”,我们即将再度讨论典型的继承滥用问题,如何使用对象组合的方式,做到在运行时装饰类。为什么呢?一旦你熟悉了装饰的技巧,你将能够在不修改任何底层代码的情况下,给你的(或别人的)对象赋予新的职责
星巴兹咖啡
1、这是一个以扩张速度最快而闻名的咖啡连锁店
2、因为扩张速度实在太快了,他们准备更新订单系统,以合乎他们的饮料供应要求
原先的类设计是这样的
1、Beverage(饮料)是一个抽象类,店内所提供的饮料都必须继承自此类
2、这个名为description(叙述)的实例变量,由每个子类设置,用来描述饮料,例如“超优深焙(Dark Roast)咖啡豆”
3、cost()方法是抽象的子类必须定义自己的实现
4、每个子类实现cost()来返回饮料的价钱
需求来了
购买咖啡时,也可以要求在其中加入各种调料,例如:蒸奶(Steamed Milk)、豆浆(Soy)、摩卡(Mocha,也就是巧克力风味)或覆盖奶泡。星巴兹会根据所加入的调料收取不同的费用。所以订单系统必须考虑到这些调料部分
这是第一次尝试
1、每个cost()方法将计算出咖啡加上订单上各种调料的价钱
2、哇塞!这简直是“类爆炸”
3、很明显,星巴兹为自己制造了一个维护噩梦。如果牛奶的价钱上扬,怎么办?新增一种焦糖调料风味时,怎么办?
是呀,干嘛这几这么多类?利用实例变量和继承,就可以追踪这些调料呀!
好吧!就来试试看,先从Beverage基类下手,加上实例变量代表是否加上调料(牛奶、豆浆、摩卡、奶泡... ...)
1、各种调料的新的布尔值
2、现在,Beverage类中的cost()不再是一个抽象方法,我们提供了cost()的实现,让它计算要加入各种饮料的调料价钱。子类仍将覆盖cost(),但是会调用超类的cost(),计算出基本饮料加上调料的价钱
3、这些方法取得和设置调料的布尔值
1、现在加入子类,每个类代表菜单上的一种饮料
2、超类cost()将计算所有调料的价钱,而子类覆盖过的cost()会扩展超类的功能,把指定的饮料类型的价钱也加进来
3、每个cost()方法需要计算该饮料的价钱,然后通过调用超类的cost()实现,加入调料的价钱
通过思考设计将来可能需要的变化,可以看出来这种方法有一些潜在的问题
1、调料价钱的改变会使我们更改现有代码
2、一旦出现新的调料,我们就需要加上新的代码,并改变超类中的cost()方法
3、以后可能开发出新的饮料,对这些饮料而言(例如:冰茶),某些调料可能并不适合,但是在这个设计方式中,Tea(茶)子类仍将继承那些不适合的方法,例如:hasWhip()(加奶泡)
4、万一顾客想要双倍摩卡咖啡,怎么办?
大师和门徒
大师:我说蚱蜢呀!举例我们上次见面已经有些时日,你对于继承的冥想,可有精进?
门徒:是的,大师。尽管继承威力强大,但是我体会到它并不总是能够实现最优弹性和最好维护的设计
大师:啊!是的,看来你已经有所长进。那么,告诉我,我的门徒,不通过继承又能如何达到复用呢?
门徒:大师,我们已经了解到利用组合(composition)和委托(delegation)可以在运行时具有继承行为的效果
大师:好,好,继续... ...
门徒:利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态地进行扩展
大师:很好,蚱蜢,你已经开始看到组合得为例了
门徒:是的,我可以利用此技巧把多个新职责,甚至是设计超类时还没有想到的职责加在对象上。而且,可以不用修改原来的代码
大师:利用组合维护代码,你认为效果如何?
门徒:这正是我要说的。通过动态地组合对象,可以写新的代码添加新功能,而无需修改现有代码。既然没有改变现有代码,那么引进bug或产生以外副作用的机会将大幅度减少
大师:非常好。蚱蜢今天的谈话就到这里。希望你能在这个主题上更深入... ... 牢记,代码应该如同晚霞中的睡莲一样地关闭(免于改变),如同晨曦中的莲花一样地开放(能够扩展)
开放——关闭原则
1、设计原则——类应该对扩展开放,对修改关闭
2、请进,现在"开放"中,欢迎用任何你想要的行为来扩展我们的类。如果你的需要或需求有所改变(我们知道这一定会发生的),那就来吧!动手扩展吧!
3、抱歉,现在是"关闭"状态。没错。我们花了许多时间得到了正确的代码,还解决了所有的bug,所以不能让你修改现有的代码。我们必须关闭代码以防止被修改。如果你不喜欢,可以找经理谈
4、我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。如能实现这样的目标,有什么好处呢?这样的设计具有弹性可以应对改变,可以接受新的功能来应对改变的需求
对扩展开放,对修改关闭?听起来很矛盾。设计如何兼顾两者?
1、这是一个很好的问题,咋听之下,的确感到矛盾,毕竟,越难修改的事物,就越难以扩展,不是吗?
2、但是,有些聪明的OO技巧,允许系统在不修改代码的情况下,进行功能扩展。想想观察者模式,通过加入新的观察者,我们可以在任何时候扩展Subject(主题),而且不需向主题中添加代码。以后,你还会陆续看到更多的扩展行为的其他OO设计技巧
好吧!我了解观察者模式,但是该如何将某件东西设计成可以扩展,又禁止修改?
许多模式是长期经验的实证,可通过提供扩展的方法来保护代码免于被修改。
装饰者模式就是遵循开放——关闭原则的一个很好的例子
我如何让设计的每个部分都遵循开放——关闭原则?
通常,你办不到。要让OO设计同时具备开放性和关闭性,又不修改现有的代码,需要花费许多时间和努力。一般来说,我们实在没有闲工夫把设计的每个部分都这么设计(而且,就算做得到,也可能只是一种浪费)。遵循开放——关闭原则,通常会引入新的抽象层次,增加代码的复杂度。你需要把注意力集中在设计中最有可能改变的地方,然后应用开放——关闭原则
我怎么知道,哪些地方的改变是更重要呢?
这牵涉到设计OO系统的经验,和对你工作领域的了解。多看一些其他的例子可以帮你学习如何辨别设计中的变化区
总结
虽然似乎有点矛盾,但是的确有些技术可以允许在不直接修改代码的情况下对其进行扩展
在选择需要被扩展的代码部分时要小心。每个地方都采用开放——关闭原则时一种浪费,也没必要,还会导致代码变得复杂且难以理解
认识装饰者模式
1、装饰者和被装饰对象有相同的超类型
2、你可以用一个或多个装饰者包装一个对象
3、既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它
4、装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目的
5、对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象
定义装饰者模式
装饰者模式——动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案
1、每个组件都可以单独使用,或者被装饰器包起来使用
2、ConcreteComponent是我们将要动态地加上新行为的对象,它扩展自Component
3、每个装饰者都"有一个"(包装一个)组件,也就是说,装饰者有一个实例变量以保存某个Component的引用
4、装饰者功能共同实现的接口(也可以是抽象类)
5、ConcreteDecorator有一个实例变量,可以记录所装饰的事物(装饰者包着的Component)
6、装饰者可以扩展Component的状态
7、装饰者可以加上新的方法。就行为是通过在旧行为前面或后面做一些计算来添加的
装饰我们的饮料
哎呀!我有点混淆... ..我原以为在这个模式中不会使用继承,而是要利用组合取代继承
Sue:这话怎么说?
Mary:看看类图。CondimentDecorator扩展自Beverage类,这用到了继承,不是吗?
Sue:的确是如此,但我认为,这么做的重点在于,装饰者和被装饰者必须是一样的类型,也就是有共同的超类,这是相当关键的地方。在这里,我们利用继承达到“类型匹配”,而不是利用继承获得“行为”
Mary:我知道为何装饰者需要和被装饰者(即被包装的组件)有相同的“接口”,因为装饰者必须能取代被装饰者,但是行为又是从哪里来的?
Sue:当我们将装饰者与组件组合时,就是在加入新的行为。所得到的新行为,并不是继承自超类,而是由组合对象的来的
Mary:好的。继承Beverage抽象类,是为了有正确的类型,而不是继承它的行为。行为来自装饰者和基础组件,或与其他装饰者之间的组合关系
Sue:正是如此
Mary:哦!我明白了。而且因为使用对象组合,可以把所有饮料和调料更有弹性地加以混合与匹配,非常方便
Sue:是的。如果依赖继承,那么类得行为只能在编译时静态决定。换句话说,行如果不是来自超类,就是子类覆盖后的版本。反之,利用组合,可以把装饰者混合着用... ...而且是在"运行时"
Mary:而且,如我所理解的,我们可以在任何时候,实现新的装饰者增加新的行为。如果依赖继承,每当需要新行为时,还的修改现有的代码
Sue:的确如此
Mary:我还剩下一个问题,如果我们需要继承的是component类型,为什么不Beverage类设计成一个接口,而是设计成一个抽象类呢?
Sue:关于这个嘛,还记得吗?当初我们从星巴兹拿到这个程序时,Beverage已经是一个抽象类了。通常装饰者模式是采用抽象类,但是在Java中可以使用接口。尽管如此,通常我们都努力避免修改现有的代码,所以,如果抽象类运作的很好,还是别去修改它
装饰者的告白
HeadFirst:欢迎装饰者模式,听说你最近情绪有点差?
装饰者:是的,我知道大家都认为我是一个有魅力的设计模式,但是,你知道吗?我也有自己的困扰,就和大家一样
HeadFirst:愿意让我们分担一些你的困扰吗?
装饰者:当然可以。你知道我有能力为设计注入弹性,这是毋庸置疑的,但是我也有“黑暗面”。有时候我会在设计中加入大量的小类,这偶尔会导致别人不同意了解我的设计方式
HeadFirst:你能够举个例子吗?
装饰者:以Java I/O库来说,人们第一次接触到这个库时,往往无法轻易地理解它。但是如果他们能认识到这些类都是用来包装InputStream的,一切都会变得简单多了
HeadFirst:听起来并不严重。你还是一个很好的模式,只需要一点点的教育,让大家知道怎么用,问题就解决了
装饰者:恐怕不止这些,我还有类型问题。有些时候,人们在客户代码中依赖某种特殊类型,然后忽然导入装饰者,却又没有周详地考虑一切。现在,我的一个优点是,你通常可以透明地插入装饰者,客户程序甚至不需要知道它是在和装饰者打交道。但是,如我刚刚所说的,有些代码会依赖特定的类型,而这样的代码一导入装饰者,嘭!出状况了!
HeadFirst:这个嘛,我相信每个人都必须了解到,在插入装饰者时,必须要小心谨慎。我不认为这是你的错!
装饰者:我知道,我也试着不这么想。我还有一个问题,就是采用装饰者在实例化组件时,将增加代码的复杂度。一旦使用装饰者模式,不只需要实例化组件,还要把此组件包装进装饰者中,天晓得有几个
HeadFirst:我下周会访谈工厂(Factory)模式和生成器(Builder)模式,我听说他们对这个问题有很大的帮助
装饰者:那倒是真的。我应该常和这些家伙聊聊
HeadFirst:我们都认为你是一个好的模式,适合用来建立有弹性的设计,维持开放——关闭原则,你要开心一点,别负面思考
装饰者:我尽量把。谢谢你
再看OO原则
1、封装变化
2、多用组合,少用继承
3、针对接口编程,不针对实现编程
4、为交互对象之间的松耦合设计而努力
5、对扩展开放,对修改关闭
装饰者模式定义
动态地将责任附加到对象上。想要扩展功能,装饰者提供有别于继承的另一种选择
要点
1、继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式
2、在我们的设计中,应该允许行为可以被扩展,而无须修改现有的代码
3、组合和委托可用于在运行时动态地加上新的行为
4、除了继承,装饰者模式也可以让我们扩展行为
5、装饰者模式意味着一群装饰者类,这些类用来包装具体组件
6、装饰者类反应出被装饰的组件类型(事实上,他们具有相同的类型,都经过接口或继承实现)
7、装饰者可以在被装饰者的行为前面与/后后面加上自己的行为,甚至将被装饰者的额行为整个取代掉,而达到特定的目的
8、你可以用无数个装饰者包装一个组件
9、装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型
10、装饰者会导致设计中出现许多小对象,如果过度使用,会让你的程序变得很复杂