游戏开发设计模式3 装饰者模式
第三章 装饰者模式
在本章将会说到喜欢爱用继承的同学一个全新的设计方式。我们即将再度探讨典型的继承滥用问题。你将在本章学到如何使用对象组合的方式,做到在运行时去装饰类。为什么呢?一旦你熟悉了装饰的技巧,你将能够在不修改任何底层代码的情况下,
给你的(或别人的)对象赋子新的职责。(话不多说 直接开喷)
前期项目举例说明
有一个商店 因为生意火爆,准备修改他们的销售订单系统,他们原先的类设计是这样的
当我们购买咖啡时,也可以要求在里面加入各种调料,例如在里面加 牛奶,巧克力,奶泡等调料,商店会根据加入不同调料来收取不同的费用,所以订单系统必须考虑到调料这一部分。
首先我们看一下依照上面的类设计会怎么样
每个cost()方法将计算出咖啡加上订单上各种调料的价钱。饮料加几个就会类就会多的爆炸, 每个价格都需要单独在类里面进行计算,繁琐无比
违反设计原则
很明显,商店自己给自己挖了一个维护巨坑。想一想如果牛奶价格上涨,怎么办,新增加了一个焦糖风味怎么办?维护起来工作量爆炸。 以上类设计违反了我们之前说的两个设计原则。
好,这时候可能就会有大聪明想到,干嘛设计这么多类,利用实例变量 继承就可以追踪这些调料了鸭!
好,那我们先试试看,先从Beverage基类下手,加上实例变量代表是否加上调料(牛奶、豆浆、摩卡、奶泡等。。。。。)
通过上面的类设计时会好一些但是 通过思考设计将来可能需要的变化,我可以看出来这种方法有一些潜在的问题。
当哪些需求或因素改变时会影响这个设计?
1.调料价钱的改变会使我们更改现有代码,
2.一旦出现新的调科,我们就需要加上新的方法,并改变超类中的cost()方法
3.以后可能会开发出新饮科。对这些饮料而言(例如:冰红茶),某些调料可能并不适合,但是在这个设计方式中,Tea(茶)子类仍将继承那些不适合的方法,例如:hasWhip()(加奶泡)。
4.万一顾客想要双倍摩卡咖啡,怎么办?
开闭原则
设计原则
类应该对扩展开放,对修改关闭
我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。如能实现这样的目标,有什么好处呢?这样的设计具有弹性可以应对改变,可以接受新的功能来应对改变的需求。
但是在原有功能上修改 这是不允许的,你扩展我就开门,你修改原来的方法函数那时不行的,那我就关闭。
问:对扩展开放,对修改关闭?听起来很矛盾。设计如何兼顾两者?
答:这是一个很好的问题。乍听之下,的确感到矛盾,毕竟,越难修改的事物,就越难以扩展,不是吗?但是,有一些聪明的面向对象技巧,允许系统在不修改代码的情况下,进行功能扩展,想想观察者模式(在第2章)……通过加入新的观察者,我们可以在任何时候扩展Subject(主题),
而且不需向主题中添加代码。以后,你还会陆续看到更多的扩展行为的其他面向对象设计技巧。
问:好吧!我了解观察者(observable),但是该如何将某件东西设计成可以扩展,又禁止修改?
答:许多模式是长期经验的实证。可通过提供扩展的方法来保护代码免于被修改。在本章,将看到使用装饰者模式的一个好例子,完全遵循开放-关闭原则。
问:我如何让设计的每个部分都遵循开放-关闭原则?
答:通常,你办不到。要让面向对象设计同时具备开放性和关闭性,又不修改现有的代码,需要花费许多时间和努力。一般来说,我们实在没有闲工夫把设计的每个部分都这么设计
(而且,就算做得到,也可能只是一种浪费)。遵循开放-关闭原则,通常会引入新的抽象层次,增加代码的复杂度。你需要把注意力集中在设计中最有可能改变的地方,然后应用开放-关闭原则。
问:我怎么知道,哪些地方的改变是更重要呢?
答:这牵涉到设计面向系统的经验和对你工作领域的了解。多看一些其他的例子可以帮你学习如何辨别设计中的变化区域。
虽然似乎有点矛盾,但是的确有一些技术可以允许在不直接修改代码的情况下对其进行扩展。在选择需要被扩展的代码部分时要小心。每个地方都采用开放-关闭原则,是一种浪费,也没必要,还会导致代码交得复杂旦难以理解。
认识装饰者模式
好了,我们已经了解利用继承无法完全解决问题,在商店遇到的问题有:类数量爆炸、设计死板,以及基类加入的新功能并不适用于所有的子类。所以,在这里要采用不一样的做法:我们要以饮料为主体,然后在运行时以调料来“装饰”(decorate)饮料。比方说,
如果顾客想要摩卡和奶泡深焙咖啡,那么,要做的是:
1.拿一个深焙咖啡(DarkRoast)对象
2.以摩卡(Mocha)对象装饰它
3.以奶泡(Whip)对象装饰它
4.调用cost()方法,并依赖委托(delegate)将调料的价钱加上去
好了!但是如何“装饰”一个对象,而“委托”又要如何与此搭配使用呢?下面来瞅一瞅
以装饰者构造饮料订单
1.以DarkRoast对象开始
2.顾客想要摩卡(Mocha),所以建立一个Mocha对象,并用它将DarkRoast对象包(wrap)起来。
3.顾客也想要奶泡(Whip),所以需要建立一个Whip装饰者,并用它将Mocha对象包起来。别忘了,DarkRoast继承自Beverage,且有一个cost()方法,用来计算饮料价钱。
4.现在,该是为顾客算钱的时候了。通过调用最外圈装饰者(Whip)的cost()就可以办得到。Whip的cost()会先委托它装饰的对象(也就是Mocha)计算出价钱然后再加上奶泡的价钱。
好了,这是目前所知道的一切……
1.装饰者和被装饰对象有相同的超类型。
2.你可以用一个或多个装饰者包装一个对象。
3.既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合可以用装饰过的对象代替它。
4.装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目的。
5.对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象。
现在、就来看看装饰者模式的定义,并写一些代码,了解它到底是怎么工作的。
定义装饰者模式
让我们先来看看装饰者模式的说明:
装饰者模式动态地将责任附加到对象上:若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
虽然这说明了装饰者模式的“角色”,但是没说明怎么在我们的实现中实际“应用”它。我们来看看类图,会有些帮助
装饰我们的饮料
好吧!让商店饮料也能符合此框架…..
在往下看之前,想想如何实现咖啡和调料的Cost()方法。也思考一下如何实现调料的(描述)getDescription()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// <summary>
/// Beverage是一个抽象类
/// </summary>
public abstract class Beverage
{
string description = "未知的饮料";
/// <summary>
/// getDescription()已经在此实现了但是cost()必须在子类中实现。
/// </summary>
/// <returns></returns>
public virtual string GetDescription()
{
return description;
}
public abstract double Cost();
}
/// <summary>
/// Beverage很简单。让我们也来实现Condiment(调料)抽象类,也就是装饰者类吧:
/// 首先,必须让Condiment Decorator能够取代Beverage,所以将CondimentDecorator扩展自 Beverage 类。
/// </summary>
public abstract class CondimentDecorator : Beverage
{
}
实现饮料
好,现在开始写饮料的代码
现在,已经有了基类,让我们开始开始实现一些饮料吧!先从浓缩咖啡(Espresso)开始。别忘了,我们需要为具体的饮料设置描述,而且还必须实现cost()方法。
饮料类实现 先写俩个 后面都是基本一样的,改下描述和价格就Ok
1 | /// <summary> |
实现调料
写调料代码
如果你回头去看看装饰者模式的类图、将发现我们已经完成了抽象组件(Beverage),有了具体组件(HouseBlend),也有了抽象装饰者(condimentDecorator)。现在,我们就来实现具体装饰者。先从摩卡下手:
1 | /// <summary> |
测试调料
供应咖啡
这是用来下订单的一些测试代码*
1 | public class StarbuzzCoffee : MonoBehaviour |
当我们介绍到“工厂”和“生成器”设计模式时,将有更好的方式建立被装饰者对象。
现在,来看看实验结果(实现思路相当于物体不断的包含子物体添加 最后累加起来输出):
尾章 复习
来到第3章的结尾,我们的设计原则又多了一些东西…· 从头捋一下 先说我们的OO设计原则
封装变化
多用组合,少用继承
针对接口编程,不针对实现编程
为对象之间的松耦合而努力
对扩展开放,对修改关闭(新增 开闭原则)
再说我们的00设计模式
装饰者模式–动态地将责任附加到对象上。想要扩展功能,装饰者提供有别于继承的另一种选择。
要点
1.继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式。
2.在我们的设计中,应该允许行为可以被扩展,而无需修改现有代码
3.组合和委托可用于在运行时动态加上新的行为
4.除了继承 装饰者模式也可以扩展行为
5.装饰者模式意味者一群装饰者类,这些类用来包装具体组件
6.装饰者类反映出被装饰者的组件类型(事实上,他们具有相同的类型,都经过接口或者继承的实现)。
7.装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。
8.你可以用无数个装饰者包装一个组件。
9.装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型。
10.装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂。