第十五章 装饰者模式
在程序开发中,很多时候并不希望某个类天生就非常庞大,一次性包含许多职责。装饰者模式可以动态给某个对象添加一些额外的职责,不会影响从这个类中派生出的其他对象。
在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活,还会带来许多问题:以方便导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变;另一方面,超类的内部细节是对子类可见的,继承这种功能复用方式常常被认为破坏了封装性。
使用继承完成一些功能复用时,有可能创建出大量的子类,使子类的数量爆炸性增长。
装饰者模式能在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
15.1 模拟传统面向对象语言的装饰者模式
作为一门解释执行的语言,给 JavaScript 中的对象动态添加或者改变职责是一件简单的事情,虽然这种做法改动了对象自身,跟传统定义中的装饰者模式不一样,但更符合 JavaScript 的语言特色。
1 | var obj = { |
传统面向对象语言的装饰者模式模拟:
1 | var Plane = function(){} |
导弹类和原子弹类的构造函数都接受参数 plane
对象,并且保存好这个对象,在它们的 fire
方法中,除了执行自身的操作之外,还调用 plane
对象的 fire
方法。
这种给对象动态增加职责的方式,没有真正改动对象自身,而是将对象放入另一个对象中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都有相同的接口(fire
方法),当请求到达链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。
因为装饰者和它所装饰的对象有一致的接口,所以对使用该对象的客户来说是透明的,被装饰的对象也不需要了解它曾被装饰过,这种透明性使得我们可以递归地嵌套任意多个装饰者对象。
1 | var plane = new Plane() |
15.2 装饰者也是包装器
从功能上看,decorator 能很好地描述这个模式,从结构上看,wrapper 的说法更加贴切。装饰者模式将一个对象嵌入另一个对象中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。请求随着这条链依次传递给所有对象,每个对象都有处理这条请求的机会。
15.3 JavaScript 的装饰者
JavaScript 语言动态改变对象相当容易,可以直接改写对象或者对象的某个方法,不需要用“类”来实现装饰者模式:
1 | var plane = { |
15.4 装饰函数
需要一个办法,在不改变原函数代码的情况下增加功能。
可通过保存原引用的方式改写某个函数:
1 | window.onload = function() {} |
但是这种方式存在两个问题:
- 必须维护
_onload
中间变量,如果函数的装饰链较长,或者需要装饰的函数变多,中间变量的数量也会变多。 this
被劫持。在window.onload
中没有,是因为调用普通函数_onload
时,this
也指向window
,跟调用window.onload
时一样。现在把window.onload
换成document.getElementById
1 | var _getElementById = document.getElementById |
此时的 _getElementById
是一个全局函数,this
指向 window
,而 document.getElementById
方法的内部实现需要使用 this
引用,this
在这个方法内部预期是指向 document
而不是 window
。
需要手动把 document
当作上下文 this
传入 _getElementById
:
1 | var _getElementById = document.getElementById |
15.5 用 AOP 装饰函数
1 | Function.prototype.before = function(beforefn) { |
回到之前的例子:
1 | window.onload = function() { |
上面的实现是在 Function.prototype
上添加 before
和 after
方法,但许多人不喜欢这种污染原型的方法,可以把原函数和新函数都作为参数传入:
1 | var before = function(fn, beforefn) { |
15.6 应用实例
15.6.1 数据统计上报
1 | var showLogin = function() { |
15.6.2 用 AOP 动态改变函数的参数
1 | Function.prototype.before = function(beforefn) { |
在 1 和 2 处可以看到,beforefn
和原函数 __self
共用一组参数列表 arguments
,当在 beforefn
函数体内改变 arguments
的时候,原函数 __self
接收的参数列表也会变化。
下面例子展示如何通过 Function.prototype.before
方法给函数 func
的参数 param
动态添加属性 b:
1 | var func = function(param) { |
如,用 AOP 方法给 ajax 函数动态装饰上 Token
参数,保证 ajax 函数是一个相对纯净的函数,提高其复用性。
1 | var ajax = function(type, url, param) { |
15.6.3 插件式的表单验证
分离校验输入和提交 ajax 请求的代码。
1 | Function.prototype.before = function(beforefn) { |
validate
成为一个即插即用的函数,甚至可以被写成配置文件的形式,有利于我们分开维护。再利用策略模式稍加改造,就可以把这些校验规则都写成插件的形式,用在不同的项目中。
因为新函数通过 Function.prototype.before
或者 Function.prototype.after
被装饰之后,返回的实际上是一个新函数,如果在原函数上保存了一些属性,那么这些属性会丢失。
另外,这种装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会有影响。
15.7 装饰者模式和代理模式
装饰者模式和代理模式最重要的区别在于它们的意图和设计目的。
代理模式的目的是,当直接访问本体不方便或者不符合需求时,为这个本体提供一个替代者。本体定义了关键功能,代理提供或者拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是对对象动态加入行为。代理模式强调一种关系,这种关系可以静态的表达,即一开始就可以被确定。装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层“代理-本体”的引用,装饰者模式经常会形成一条装饰链。