Skip to content

装饰者模式(Decorator)

装饰者模式可以动态给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象

还有一种添加功能的方式是继承。继承这种功能复用方式通常被称为”白箱复用“,”白箱“是相对可见性而言的,在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性。

装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责

设计原则:多用组合,少用继承。

优点

  • 动态为对象添加行为

缺点

  • 往往会形成一条长长的装饰链,会对性能上有一定的影响。

JS模拟传统面向对象语言中的装饰者模式

:一辆战斗机一开始是发送普通子弹,随着升级开始发送原子弹和导弹。

javascript
const Plane  = function() {}

Plane.prototype.fire = function() {
  console.log('发射普通子弹');
}

const MissileDecorator = function(plane) {
  this.plane = plane
}

MissileDecorator.prototype.fire = function() {
  this.plane.fire()
  console.log('发射导弹');
}

const AtomDecorator = function(plane) {
  this.plane = plane
}

AtomDecorator.prototype.fire = function() {
  this.plane.fire()
  console.log('发射原子弹');
}

let plane = new Plane()
plane = new MissileDecorator(plane)
plane = new AtomDecorator(plane)
plane.fire()

这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。

装饰者模式将一个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。请求随着这条链依次传递到所有的对象,每个对象都有处理这条请求的机会。

装饰者模式

在JS中的实现

比如在 window 绑定 onload 事件,但又不确定这个时间是不是已经被其他人绑定过,为了避免覆盖掉之前的 onload 函数中的行为,所以事先会保存原来的 onload 函数。

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

const _onload = window.onload || function() {} // 保存原先 onload 函数

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

存在两个问题

  • 必须维护中间变量(如:_onload
  • 有时候会存在 this 指向问题。

用AOP装饰函数解决

javascript
Function.prototype.before = function (beforefn) {
  const _self = this // 保存原函数的引用
  return function () {
    beforefn.apply(this, arguments) // 执行新函数
    return _self.apply(this, arguments) // 并返回原函数的结果
  }
}

Function.prototype.after = function (afterfn) {
  const _self = this
  return function () {
    const ret = _self.apply(this, arguments) // 并返回原函数的结果
    afterfn.apply(this, arguments) // 执行新函数
    return ret
  }
}

// -------------分割线---------------
// 有一些人不喜欢这种污染原型的方法,也可以改成这样:
const before = function (fn, beforefn) {
    return function () {
        beforefn.apply(this, arguments) // 执行新函数
    	return fn.apply(this, arguments) // 并返回原函数的结果
    }
}
// 使用的时候 a = before(a, function () {})

使用

html
<button id="btn">按钮</button>

  <script>
    Function.prototype.before = function (beforefn) {
      const _self = this // 保存原函数的引用
      return function () {
        beforefn.apply(this, arguments) // 执行新函数
        return _self.apply(this, arguments) // 并返回原函数的结果
      }
    }

    Function.prototype.after = function (afterfn) {
      const _self = this
      return function () {
        const ret = _self.apply(this, arguments) // 并返回原函数的结果
        afterfn.apply(this, arguments) // 执行新函数
        return ret
      }
    }

    document.getElementById = document.getElementById.before(function () {
      alert(1)
    })

    const btn = document.getElementById('btn');
  </script>

  <!-- 打开浏览器后,会弹出警告框,因为在getElementById这个方法添加了弹出的功能 -->

应用实例

数据上报

比如要统计登录按钮被点击了多少次,按一般的思路,我们会在按钮的点击事件中调用数据上报的方法,如:

javascript
document.getElementById('btn').addEventListener('click', function () {
    // TODO 打开登录弹窗
    // TODO 数据上报
})

毫无疑问,这么实现是没有问题的,但两个层面的的功能被耦合在一个函数里,既要打开弹窗,又要上报。

我们可以使用 AOP 分离:

javascript
Function.prototype.after = function (afterfn) {
      const _self = this
      return function () {
        const ret = _self.apply(this, arguments)
        afterfn.apply(this, arguments)
        return ret
      }
}

let handleLogin = () => {
    // TODO 打开登陆弹窗
}

const log = () => {
    // TODO 数据上报
}

handleLogin = handleLogin.after(log)
document.getElementById('btn').addEventListener('click', handleLogin)

完成~😊

插件式表单验证

javascript
Function.prototype.before = function (beforefn) {
  const _self = this
  return function () {
    if (beforefn.apply(this, arguments) === false) {
      // 当beforefn 返回 false 的情况直接return,不执行后续原函数
      return
    }
    return _self.apply(this, arguments)
  }
}

const validata = function () {
  if (username.value === '') {
    alert('用户名不能为空')
    return false
  }
  if (password.value === '') {
    alert('密码不能为空')
    return false
  }
}

const formSubmit = function () {
  // TODO fetch login
}

formSubmit = formSubmit.before(validata)

btn.onclick = function () {
  formSubmit()
}

注意:函数通过 Function.prototype.before 或者 Function.prototype.after 被装饰之后,返回的实际上是一个新的函数,如果原函数上保存了一些属性,那么这些属性会丢失。

装饰者模式和代理模式的区别

这两种模式都描述了为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求。

区别

  • 代理模式目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情
  • 装饰者模式的作用就是为对象动态加入行为

代理模式通常只是一层代理-本体的引用,而装饰者模式经常会形成一条长长的装饰链。

Released under the MIT License.