前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >装饰者模式

装饰者模式

作者头像
一粒小麦
发布2019-11-19 17:53:55
5370
发布2019-11-19 17:53:55
举报
文章被收录于专栏:一Li小麦

装饰者模式

"封装,继承,多态"是面向对象的三大特点。

"美丽,知性,善良"是(我以为)的完美女性的三大要素。

你在夸赞一个女性可以说,她是见过的女人中"最美丽,最知性,最善良"的。但你在夸赞一个程序员时,不能说他写的程序"最封装,最继承,最多态"。因为这三者是相生相克的。

开发过程中,我们都不希望某些类一开始就特别庞大。一次性包含太多职责。这时就不得不去面对三大特性的权衡。

继承的缺陷

在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活,还会带来许多问题:一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变;另一方面,继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的,在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性

使用继承还会带来另外一个问题,在完成一些功能复用的同时,有可能创建出大量的子类,使子类的数量呈爆炸性增长。比如现在有4种型号的相机,我们为每种相机都定义了一个单独的类。现在要给每种相机都装上镜头、滤镜和挂带这3种配件。如果使用继承的方式来给每种相机创建子类,则需要4×3=12个子类。

但是如果把镜头、滤镜和挂带这些对象动态组合到相机上面,则只需要额外增加3个类。

这种给对象动态地增加职责的方式称为装饰者(decorator)模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式,比如天冷了就多穿一件外套,需要飞行时就在头上插一支竹蜻蜓。

面向对象的装饰者

假如你现在现在在写一个打飞机游戏。飞机一开始火力是普通子弹,升2级后是导弹,3级后是核导弹。用装饰者的写法是:

代码语言:javascript
复制
class Plane{
  fire(){
    console.log('发送普通子弹')
  }
}

class Missibe{
  constructor(plane) {
    this.plane=plane;
  }
  fire(){
    this.plane.fire()
    console.log('发送导弹')
  }
}


class Atom{
  constructor(plane) {
    this.plane=plane;
  }
  fire(){
    this.plane.fire()
    console.log('发送核导弹')
  }
}

const plane=new Plane()
const missibePlane=new Missibe(plane)
const atom=new Atom(missibePlane)

atom.fire()

这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。因为装饰者对象和它所装饰的对象拥有一致的接口,所以它们对使用该对象的客户来说是透明的,被装饰的对象也并不需要了解它曾经被装饰过,这种透明性使得我们可以递归地嵌套任意多个装饰者对象。

javascript的装饰模式

高阶函数

在js中并不一定需要那些繁杂的类。上面的功能完全可以用高阶函数来实现。

代码语言:javascript
复制
const plane={
  fire(){
    console.log('普通子弹')
  }
}

const missible=(fn)=>{
  fn();
  console.log('导弹')
}

const atom=(fn)=>{
  fn();
  console.log('核导弹')
}

在JavaScript中,几乎一切都是对象,其中函数又被称为一等对象。你可以很方便地给某个对象扩展属性和方法,但却很难在不改动某个函数源代码的情况下,给该函数添加一些额外的功能。在代码的运行期间,我们很难切入某个函数的执行环境。

最差的选择是重新调整该函数。但这违反了开放封闭原则。而且在多人协作中,我们总是不愿意去动别人写的东西。也许实现某个功能的逻辑躺在某个阴暗杂乱的角落。你把它拿出来暴晒,可能会动了整个架构的老命。

这时候你要加功能,用个高阶函数包裹即可。这样你就不必去动别人的功能了。

但是类似打飞机游戏中,你不得不去维护missible和atom两个中间量。另一方面,还面临this指向的问题。

接下来让我们一劳永逸地解决this劫持的问题。

AOP函数

Aop又叫面向切面编程,其中“通知”是切面的具体实现,分为before(前置通知)、after(后置通知)、around(环绕通知),用过spring的同学肯定对它非常熟悉,而在js中,AOP是一个被严重忽视的技术点。

比如dojo中aop的实现在dojo/aspect模块中,主要有三个方法:before、after、around。

(重点)试在函数(Function)实现一个before和after方法,分别对应函数执行完之前和执行完之后。

代码语言:javascript
复制
Function.prototype.before=function(fn){
  const _this=this;
  return function(){
    fn.apply(this,arguments);
    return _this.apply(this,arguments)
  }
}


Function.prototype.after=function(fn){
  const _this=this;
  return function(){
    const ret=_this.apply(this,arguments);
    fn.apply(this,arguments);
    return ret;
  }
}

Function.prototype.before接受一个函数当作参数,这个函数即为新添加的函数,它装载了新添加的功能代码。接下来把当前的this保存起来,这个this指向原函数,然后返回一个“代理”函数,这个“代理”函数只是结构上像代理而已,并不承担代理的职责(比如控制对象的访问等)。它的工作是把请求分别转发给新添加的函数和原函数,且负责保证它们的执行顺序,让新添加的函数在原函数之前执行(前置装饰),这样就实现了动态装饰的效果。

通过Function.prototype.apply来动态传入正确的this,保证了函数在被装饰之后,this不会被劫持。

应用

用AOP装饰函数的技巧在实际开发中非常有用。不论是业务代码的编写,还是在框架层面,我们都可以把行为依照职责分成粒度更细的函数,随后通过装饰把它们合并到一起,这有助于我们编写一个松耦合和高复用性的系统。

案例1:window.onload改造

众所周知,window.onload只会执行一次。如果你要二次调用,怎么处理?

最原始的方法就是在原来的onload中追加。太low了。

但是如果用after来写,就很舒服了。

代码语言:javascript
复制
window.onload=function(){
  console.log(1)
}

window.onload=(window.onload||function(){}).after(function(){
  // 追加的代码
})
案例2:统计数据上报

现有一个按钮,功能已经开发完成。

代码语言:javascript
复制
let btnFn=()=>{
  //...
}
document.querySelector('#btn',btnFn)

这时需求找到你:要加多一个点击统计功能。如果你直接在btnFn继续写统计,是不合适的。这是两个层面的功能,却被耦合在一个函数里。应该考虑aop分离。

代码语言:javascript
复制
let log=()=>{
  // 统计逻辑
}
btnFn=btnFn.after(log);

操作就是这么骚。

案例3:用aop动态改变函数的参数

假设你在使用一串模板方法,在异步请求前,你给before传入一段函数。

代码语言:javascript
复制
let fnAjax=function(param){
  console.log(arguments)
  console.log(param)
}

fnAjax=fnAjax.before(function(param){
  param.username='djtao';
})

fnAjax({password:123456})
// Object {password: 123456, username: "djtao"}

你可以用它来为你的ajax请求预处理参数。比如带个token什么的。

案例4:插件式的表单校验

假设你的点击提交时都要校验,如果你把validate写在submit处理函数中,这段代码没有任何可复用性。

你可以把改写一下before:

代码语言:javascript
复制
Function.prototype.before=function(fn){
  const _this=this;
  return function(){
    if(fn.apply(this,arguments)===false){
      return
    }
    return _this.apply(this,arguments)
  }
}

那么你的validate方法就可以用了。

代码语言:javascript
复制
let validate=function(params){
  const {username,password}=params
  if(username==''){
    alert('用户名不得为空');
    return false
  }
  if(password==''){
    alert('密码不得为空');
    return false;
  }
}

login=login.before(validate)

在这段代码中,校验输入和提交表单的代码完全分离开来,它们不再有任何耦合关系,formSubmit=formSubmit.before(validata)这句代码,如同把校验规则动态接在formSubmit函数之前,validata成为一个即插即用的函数,它甚至可以被写成配置文件的形式,这有利于我们分开维护这两个函数。再利用策略模式稍加改造,我们就可以把这些校验规则都写成插件的形式,用在不同的项目当中。

和代理模式的区别

装饰者模式和代理模式的结构看起来非常相像,这两种模式都描述了怎样为对象提供一定程度上的间接引用,它们的实现部分都保留了对另外一个对象的引用,并且向那个对象发送请求。

代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy与它的实体之间的关系),这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理本体的引用,而装饰者模式经常会形成一条长长的装饰链。

在虚拟代理实现图片预加载的例子中,本体负责设置img节点的src,代理则提供了预加载的功能,这看起来也是“加入行为”的一种方式,但这种加入行为的方式和装饰者模式的偏重点是不一样的。装饰者模式是实实在在的为对象增加新的职责和行为,而代理做的事情还是跟本体一样,最终都是设置src。但代理可以加入一些“聪明”的功能,比如在图片真正加载好之前,先使用一张占位的loading图片反馈给客户。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-11-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 装饰者模式
    • 继承的缺陷
      • 面向对象的装饰者
        • javascript的装饰模式
          • 高阶函数
          • AOP函数
        • 应用
          • 案例1:window.onload改造
          • 案例2:统计数据上报
          • 案例3:用aop动态改变函数的参数
          • 案例4:插件式的表单校验
        • 和代理模式的区别
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档