8000 结构型模式-装饰器模式 · Issue #55 · hawtim/hawtim.github.io · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
结构型模式-装饰器模式 #55
Open
@hawtim

Description

@hawtim

装饰器模式是一种结构型模式。也可以叫做包装器。
通过这种模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类派生出来的其他对象。

在传统面向对象语言中,给对象添加新的功能往往用的是继承的方式,但是继承的方式并不灵活,并且存在以下的问题:

  1. 超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变
  2. 在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性
  3. 在完成一些功能复用的同时,有可能创建出大量的子类,使子类的数量呈爆炸性增长

关于第三点,《JavaScript设计模式与开发实践》中有个比较简洁生动的例子:

比如现在有4种型号的自行车,我们为每种自行车都定义了一个单独的类。
现在要给每种自行车都装上前灯、尾灯和铃铛这3种配件。
如果使用继承的方式来给每种自行车创建子类,则需要4×3 = 12 个子类。
但是如果把前灯、尾灯、铃铛这些对象动态组合到自行车上面,则只需要额外增加3个类

在 js 中,装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式

飞机大战的例子(传统面向对象语言的装饰者模式)

var Plane = function() {}
Plane.prototype.fire = function() {
  console.log('发射普通子弹')
}
// 增加两个装饰类,分别是导弹和原子弹
var MissileDecorator = function(plane) {
  this.plane = plane
}
MissileDecorator.prototype.fire = function(plane) {
  this.plane.fire()
  console.log('发射导弹')
}
var AtomDecorator = function(plane) {
  this.plane = plane
}
AtomDecorator.prototype.fire = function() {
  this.plane.fire()
  console.log('发射原子弹')
}

var plane = new Plane()
var missilePlane = new MissileDecorator(plane)

missilePlane.fire() // 发射导弹

因为装饰者对象和被装饰者对象拥有一致的接口,所以我们还可以嵌套多个装饰者对象,如

var plane = new Plane()
plane = new MissileDecorator(plane)
plane = new AtomDecorator(plane)
plane.fire() // 输出 发射普通子弹,发射导弹,发射原子弹

嵌套之后,实际上就是对象的作用域链又多了一层,利用对象寻找属性是基于作用域链查找的特点,我们可以在添加的那一层链上增加新的方法来实现“装饰”效果

改成 JavaScript 版本的装饰者

在 js 中可以动态改变对象,所以在 js 中可以不需要用到类这种概念

var plane = {
  fire: function(){
    console.log('发射普通子弹');
  }
}

var missileDecorator = function(){
  console.log('发射导弹');
}

var atomDecorator = function(){
  console.log('发射原子弹');
}

var fire1 = plane.fire;

plane.fire = function(){
  fire1();
  missileDecorator();
}

var fire2 = plane.fire;

plane.fire = function(){
  fire2();
  atomDecorator();
}

plane.fire();
// 分别输出:发射普通子弹、发射导弹、发射原子弹

AOP 装饰函数

在我们维护他人代码的时候,往往会出现原函数非常复杂不好改动的问题,这种时候我们就需要用保存旧函数引用的方式来处理。
比如我们想给window绑定onload事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的window.onload函数中的行为,我们一般都会先保存好原先的window.onload,把它放入新的window.onload里执行:

window.onload = function(){
  alert (1);
}

var _onload = window.onload || function(){};

window.onload = function(){
  _onload();
  alert (2);
}

以上这种情况

  1. 需要一个中间变量,如果装饰多了,中间变量也会多。
  2. 还会遇到 this 劫持问题
<script>
var _getElementById = document.getElementById;

document.getElementById = function( id ){
  alert (1);
  return _getElementById( id );       // (1)
}

var button = document.getElementById( 'button' );
</script>

这时候因为 getElementById 的作用域链在全局下,内部的 this 指向了 window,就会导致报错,需要手动 apply

return _getElementById.apply(document, arguments)

因此我们需要一个更优雅的方式来解决这个问题

AOP 装饰函数

Function.prototype.before = function(beforeFn) {
  // 保存原函数的引用
  var _self = this
  // 返回包含原函数和新函数的代理函数
  return function() {
    // 执行新函数,修正 this
    beforeFn.apply(this, arguments)
    // 执行原函数
    return _self.apply(this, arguments)
  }
}

Function.prototype.after = function(afterFn) {
  var _self = this
  return function() {
    var ret = _self.apply(this, arguments)
    afterFn.apply(this, arguments)
    return ret
  }
}

// 这时候再实现window.onload,非常简洁没有中间变量
window.onload = function(){
  alert (1);
}

window.onload = (
  window.onload || function(){}
).after(function(){
  alert (2);
}).after(function(){
  alert (3);
}).after(function(){
  alert (4);
});

上面这种方式会污染原型链,所以我们可以提供两个工具函数来实现该功能

function before(fn, beforeFn) {
  return function() {
    beforeFn.apply(this, arguments)
    return fn.apply(this, arguments)
  }
}

function after(fn, afterFn) {
  return function() {
    const ret = fn.apply(this, arguments)
    afterFn.apply(this, arguments)
    return ret
  }
}

var a = before(function() {
  console.log('3')
}, function() {
  console.log('2')
})

a = before(a, function() {
  console.log('1')
})

a = after(a, function() {
  console.log('after')
})
a() // 1, 2, 3, after

AOP 装饰函数的应用实例

  • 数据统计上报,一个函数里可能既要上报数据,又要完成其他功能,使用 aop 将上报功能剥离出来,减少业务入侵
  • 用 AOP 动态改变函数的参数,例如 封装了 ajax 的方法要动态添加参数
  • 插件式的表单验证,通过before,将验证表单数据合法的逻辑移动到函数外部

ES6 中的 decorator

引用自:阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版

[说明] Decorator 提案经过了大幅修改,目前还没有定案,不知道语法会不会再变。下面的内容完全依据以前的提案,已经有点过时了。等待定案以后,需要完全重写。

装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。许多面向对象的语言都有这项功能,目前有一个提案将其引入了 ECMAScript。

装饰器是一种函数,写成@ + 函数名。它可以放在类和类方法的定义前面。

@frozen class Foo {
  @configurable(false)
  @enumerable(true)
  method() {}
  @throttle(500)
  expensiveMethod() {}
}

上面代码一共使用了四个装饰器,一个用在类本身,另外三个用在类方法。它们不仅增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改类的功能。

优点

  • 无需创建新的子类即可扩展对象的功能
  • 能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责
  • 可以使用多个装饰者来组合多种行为
  • 符合单一职责原则

缺点

  • 在装饰器栈中删除特定装饰器比较困难
  • 使用装饰器的代码可能看起来比较难懂,ES6做了改良

使用场景

  • 希望在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为,可以使用装饰模式
  • 装饰能将业务逻辑组织为层次结构,你可为各层创建一个装饰,在运行时将各种不同逻辑组合成对象。由于这些对象都遵循通用接口,客户端代码能以相同的方式使用这些对象。
  • 如果用继承来扩展对象行为的方案难以实现或者根本不可行, 你可以使用该模式。

总结

  • 装饰者模式是一种结构型模式
  • 装饰者模式的目的是避免修改原有对象并对原有对象的功能进行动态扩展
  • 装饰者模式能够让我们将业务的功能划分出层次,通过动态添加的方式来避免更多的 if else 代码

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0