原理
装饰模式(Decorator Pattern):动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。
结构图
示例
穿衣服(python实现)
给自己搭配了一套着装:一条卡其色休闲裤、一双深色休闲皮鞋、一条银色针扣头的黑色腰带、一件紫红色针织毛衣、一件白色衬衫、一副方形黑框眼镜。但类似的着装也可以穿在其他的人身上,比如一个老师也可以这样穿:一双深色休闲皮鞋、一件白色衬衫、一副方形黑框眼镜。
我们就用程序来模拟这样一个情景。
1 | class Person: |
客户端:1
2
3
4
5
6
7
8
9
10
11
12
13def testDecorator():
tony = Engineer("Tony", "客户端")
pant = CasualPantDecorator(tony)
belt = BeltDecorator(pant)
shoes = LeatherShoesDecorator(belt)
shirt = WhiteShirtDecorator(shoes)
sweater = KnittedSweaterDecorator(shirt)
glasses = GlassesDecorator(sweater)
glasses.wear()
print()
decorateTeacher = GlassesDecorator(WhiteShirtDecorator(LeatherShoesDecorator(Teacher("wells", "教授"))))
decorateTeacher.wear()
decorateTeacher = GlassesDecorator(WhiteShirtDecorator(LeatherShoesDecorator(Teacher("wells", "教授"))))
这个写法,大家不要觉得奇怪,它其实就是将多个对象的创建过程合在了一起,其实是一种优雅的写法(是不是少了好几行代码?)。创建的 Teacher 对象又通过参数传给 LeatherShoesDecorator
的构造函数,而创建的 LeatherShoesDecorator
对象又通过参数传给WhiteShirtDecorator
的构造函数,以此类推……
输出结果:
1 | 我是客户端工程师Tony |
装饰关系VS继承关系
装饰关系:
继承关系:
装饰模式的特点
可灵活地给一个对象增加职责或拓展功能
如上面的示例中,可任意地穿上自己想穿的衣服。不管穿上什么衣服,你还是那个你,但穿上不同的衣服你就会有不同的外表。
可增加任意多个装饰
你可以只穿一件衣服,也可以只穿一条裤子,也可以衣服和裤子各种搭配的穿,全随你意!
装饰的顺序不同,可能产生不同的效果
在上面的示例中,Tony 是针织毛衣穿在外面,白色衬衫穿在里面。当然,如果你愿意(或因为怕冷),也可以针织毛衣穿在里面,白色衬衫穿在外面。但两种着装穿出来的效果,给人的感觉肯定是完全不一样的,自己脑补一下,哈哈!
使用装饰模式的方式,想要改变装饰的顺序,也是非常简单的。只要把测试代码稍微改动一下即可,如下:
1 | def testDecorator2(): |
结果如下:
1 | 我是客户端工程师Tony |
模型抽象
上图中的 Component 是一个抽象类,代表具有某中功能(function)的组件,ComponentImplA 和 ComponentImplB 分别是其具体的实现子类。Decorator 是 Component 装饰器,里面有一个 Component 的对象 decorated,这就是被装饰的对象,装饰器可为被装饰对象添加额外的功能或行为(addBehavior)。DecoratorImplA 和 DecoratorImplB 分别是两个具体的装饰器(实现子类)。
这样一种模式很好地将装饰器与被装饰的对象进行解耦。
Js中的装饰模式(Js实现)
用 AOP 装饰函数
1 | // 用 AOP 装饰函数 |
给 window 绑定 onload 事件
不使用AOP
1 | window.onload = function(){ |
使用AOP
1 | window.onload = function(){ |
不污染原型
1 | var before = function( fn, beforefn ){ |
AOP 的应用实例
数据统计上报
页面中有一个登录 button,点击这个 button 会弹出登录浮层,与此同时要进行数据上报,
来统计有多少用户点击了这个登录 button:
不使用AOP
1 | <html> |
在 showLogin 函数里,既要负责打开登录浮层,又要负责数据上报,这是两个层面
的功能,在此处却被耦合在一个函数里
使用AOP
1 | <html> |
动态改变函数的参数
观察Function.prototype.before 方法:
1 | Function.prototype.before = function (beforefn) { |
从这段代码的(1)处和(2)处可以看到,beforefn 和原函数__self
共用一组参数列表arguments,当我们在beforefn 的函数体内改变arguments 的时候,原函数__self
接收的参数列表自然也会变化。
1 | var func = function( param ){ |
在ajax请求中增加token
1 | var getToken = function () { |
从ajax 函数打印的log 可以看到,token 参数已经被附加到了ajax 请求的参数中:{name: "sven", Token: "Token"}
明显可以看到,用AOP 的方式给ajax 函数动态装饰上Token 参数,保证了ajax 函数是一
个相对纯净的函数,提高了ajax 函数的可复用性,它在被迁往其他项目的时候,不需要做任何
修改。
插件式的表单验证
我们很多人都写过许多表单验证的代码,在一个Web 项目中,可能存在非常多的表单,如
注册、登录、修改用户信息等。在表单数据提交给后台之前,常常要做一些校验,比如登录的时
候需要验证用户名和密码是否为空,代码如下:
1 |
|
formSubmit 函数在此处承担了两个职责,除了提交ajax 请求之外,还要验证用户输入的合法
性。这种代码一来会造成函数臃肿,职责混乱,二来谈不上任何可复用性。
可以将校验输入的逻辑放到validata
函数中,并且约定当validata 函数返回false 的时候,表示校验未通过,代码如下:
1 | var validata = function () { |
现在的代码已经有了一些改进,我们把校验的逻辑都放到了validata 函数中,但formSubmit函数的内部还要计算validata 函数的返回值,因为返回值的结果表明了是否通过校验。
接下来进一步优化这段代码,使validata 和formSubmit 完全分离开来。首先要改写Function.prototype.before, 如果beforefn 的执行结果返回false,表示不再执行后面的原函数,代码如下:
1 | submitBtn.onclick = function () { |
在这段代码中,校验输入和提交表单的代码完全分离开来,它们不再有任何耦合关系,formSubmit = formSubmit.before( validata )
这句代码,如同把校验规则动态接在formSubmit 函数之前,validata 成为一个即插即用的函数,它甚至可以被写成配置文件的形式,这有利于我们分开维护这两个函数。再利用策略模式稍加改造,我们就可以把这些校验规则都写成插件的形式,用在不同的项目当中
日志记录(C#实现)
现在要求我们开发的记录日志的组件,除了要支持数据库记录DatabaseLog和文本文件记录TextFileLog两种方式外,我们还需要在不同的应用环境中增加一些额外的功能,比如需要记录日志信息的错误严重级别,需要记录日志信息的优先级别,还有日志信息的扩展属性等功能。在这里,如果我们不去考虑设计模式,解决问题的方法其实很简单,可以通过继承机制去实现,日志类结构图如下:
实现如下:
1 | public abstract class Log |
需要记录日志信息的错误严重级别功能和记录日志信息优先级别的功能,只要在原来子类DatabaseLog和TextFileLog的基础上再生成子类即可,同时需要引进两个新的接口IError和IPriority,类结构图如下:
实现代码如下
1 | public interface IError |
此时可以看到,如果需要相应的功能,直接使用这些子类就可以了。这里我们采用了类的继承方式来解决了对象功能的扩展问题,这种方式是可以达到我们预期的目的。
然而,它却带来了一系列的问题:
首先,前面的分析只是进行了一种功能的扩展,如果既需要记录错误严重级别,又需要记录优先级时,子类就需要进行接口的多重继承,这在某些情况下会违反类的单一职责原则,注意下图中的蓝色区域:
实现代码:
1 | public class DBEPLog : DatabaseLog, IError, IPriority |
- 其次,随着以后扩展功能的增多,子类会迅速的膨胀,可以看到,子类的出现其实是DatabaseLog和TextFileLog两个子类与新增加的接口的一种排列组合关系,所以类结构会变得很复杂而难以维护,“子类复子类,子类何其多”
- 最后,这种方式的扩展是一种静态的扩展方式,并没有能够真正实现扩展功能的动态添加,客户程序不能选择添加扩展功能的方式和时机。
现在又该是Decorator模式出场的时候了,解决方案是把Log对象嵌入到另一个对象中,由这个对象来扩展功能。首先我们要定义一个抽象的包装类LogWrapper,让它继承于Log类,结构图如下:
实现代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class LogWrapper: Log
{
private readonly Log _log;
public LogWrapper(Log log)
{
_log = log;
}
public override void Write(string log)
{
_log.Write(log);
}
}
现在对于每个扩展的功能,都增加一个包装类的子类,让它们来实现具体的扩展功能,如下图中绿色的区域:
实现如下
1 | public class LogErrorWrapper : LogWrapper |
到这里,LogErrorWrapper类和LogPriorityWrapper类真正实现了对错误严重级别和优先级别的功能的扩展。我们来看一下客户程序如何去调用它:
1 | class Program |
注意在上面程序中的第三段装饰才真正体现出了Decorator模式的精妙所在,这里总共包装了两次:第一次对log对象进行错误严重级别的装饰,变成了logErrorWrapper2对象,第二次再对logErrorWrapper2对象进行装饰,于是变成了logPriorityWrapper2对象,此时的logPriorityWrapper2对象同时扩展了错误严重级别和优先级别的功能。也就是说我们需要哪些功能,就可以这样继续包装下去。到这里也许有人会说LogPriorityWrapper类的构造函数接收的是一个Log对象,为什么这里可以传入LogErrorWrapper对象呢?通过类结构图就能发现,LogErrorWrapper类其实也是Log类的一个子类。
我们分析一下这样会带来什么好处?首先对于扩展功能已经实现了真正的动态增加,只在需要某种功能的时候才进行包装;其次,如果再出现一种新的扩展功能,只需要增加一个对应的包装子类(注意:这一点任何时候都是避免不了的),而无需再进行很多子类的继承,不会出现子类的膨胀,同时Decorator模式也很好的符合了面向对象设计原则中的“优先使用对象组合而非继承”和“开放-封闭”原则。
.NET中的Stream(C#实现)
可以看到, BufferedStream和CryptoStream其实就是两个包装类,这里的Decorator模式省略了抽象装饰角色(Decorator),示例代码如下:
1 | class Program |
其中BufferedStream类的代码(只列出部分),它是继承于Stream类:
1 | public sealed class BufferedStream : Stream |
java中也是如此,java IO中的 FilterInputStream 和 FilterOutputStream的实现其实就是一个装饰模式。FilterInputStream(FilterOutputStream) 就是一个装饰器,而 InputStream(OutputStream) 就是被装饰的对象。
1 | DataInputStream dataInputStream = new DataInputStream(new FileInputStream("C:/text.txt")); |
优缺点
装饰模式的优点:
使用装饰模式来实现扩展比继承更加灵活,它可以在不需要创造更多子类的情况下,将对象的功能加以扩展。
可以动态地给一个对象附加更多的功能。
可以用不同的装饰器进行多重装饰,装饰的顺序不同,可能产生不同的效果。
装饰类和被装饰类可以独立发展,不会相互耦合;装饰模式相当于是继承的一个替代模式。
装饰模式的缺点:
- 与继承相比,用装饰的方式拓展功能更加容易出错,排错也更困难。对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
应用场景
有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。
需要动态地增加或撤销功能时。
不能采用生成子类的方法进行扩充时,如类定义不能用于生成子类。