使用装饰者模式做有趣的事情

什么是装饰者模式

装饰者模式是一种为函数或类增添特性的技术,它可以让我们在不修改原来对象的基础上,为其增添新的能力和行为。它本质上也是一个函数(在javascipt中,类也只是函数的语法糖)。

我们什么时候可以弄到它呢

我们来假设一个场景,一个自行车商店有几种型号的自行车,现在商店允许用户为每一种自行车提供一些额外的配件,比如前灯、尾灯、铃铛等。每选择一种或几种配件都会影响自行车的售价。

如果按照比较传统的创建子类的方式,就等于我们目前有一个自行车基类,而我们要为每一种可能的选择创建一个新的类。可是由于用户可以选择一种或者几种任意的配件,这就导致最终可能会生产几十上百个子类,这明显是不科学的。然而,对这种情况,我们可以使用装饰者模式来解决这个问题。

自行车的基类如下:

class Bicycle {
    // 其它方法
    wash () {}
    ride () {}
    getPrice() {
        return 200;
    }
}

那么我们可以先创建一个装饰者模式基类

class BicycleDecotator {
    constructor(bicycle) {
        this.bicycle = bicycle;
    }
    wash () {
        return this.bicycle.wash();
    }
    ride () {
        return this.bicycle.ride();
    }
    getPrice() {
        return this.bicycle.getPrice();
    }
}

这个基类其实没有做什么事情,它只是接受一个Bicycle实例,实现其对应的方法,并且将调用其方法返回而已。

有了这个基类之后,我们就可以根据我们的需求对原来的Bicycle类为所欲为了。比如我可以创建一个添加了前灯的装饰器以及添加了尾灯的装饰器:

class HeadLightDecorator extends BicycleDecorator {
    constructor(bicycle) {
        super(bicycle);
    }
    getPrice() {
        return this.bicycle.getPrice() + 20;
    }
}
class TailLightDecorator extends BicycleDecorator {
    constructor(bicycle) {
        super(bicycle);
    }
    getPrice() {
        return this.bicycle.getPrice() + 20;
    }
}

那么,接下来我们就可以来对其自由组合了:

let bicycle = new Bicycle();
console.log(bicycle.getPrice()); // 200
bicycle = new HeadLightDecorator(bicycle); // 添加了前灯的自行车
console.log(bicycle.getPrice());  // 220
bicycle = new TailLightDecorator(bicycle); // 添加了前灯和尾灯的自行车
console.log(bicycle.getPrice()); // 240

这样写的好处是什么呢?假设说我们有10个配件,那么我们只需要写10个配件装饰器,然后就可以任意搭配成不同配件的自行车并计算价格。而如果是按照子类的实现方式的话,10个配件可能就需要有几百个甚至上千个子类了。

从例子中我们可以看出装饰者模式的适用场合:

  1. 如果你需要为类增添特性或职责,可是从类派生子类的解决方法并不太现实的情况下,就应该使用装饰者模式。
  2. 在例子中,我们并没有对原来的Bicycle基类进行修改,因此也不会对原有的代码产生副作用。我们只是在原有的基础上增添了一些功能。因此,如果想为对象增添特性又不想改变使用该对象的代码的话,则可以采用装饰者模式。

装饰者模式除了可以应用在类上之外,还可以应用在函数上(其实这就是高阶函数)。比如,我们想测量函数的执行时间,那么我可以写这么一个装饰器:

function func() {
    console.log('func');
}
function timeProfileDecorator(func) {
    return function (...args) {
        const startTime = new Date();
        func.call(this, ...args);
        const elapserdTime = (new Date()).getTime() - startTime.getTime();
        console.log(`该函数消耗了${elapserdTime}ms`);
    }
}
const newFunc = timeProfileDecorator(func);
console.log(newFunc());

做一些有趣的事情

既然知道了装饰者模式可以在不修改原来代码的情况下为其增添一些新的功能,那么我们就可以来做一些有趣的事情。

我们可以为一个类的方法提供性能分析的功能。

class TimeProfileDecorator {
  constructor(component, keys) {
    this.component = component;
    this.timers = {};
    const self = this;
    for (let i in keys) {
      let key = keys[i];
        if (typeof component[key] === 'function') {
          this[key] = function(...args) {
            this.startTimer(key);
            // 解决this引用错误问题
            component[key].call(component, ...args);
            this.logTimer(key);
          }
        }
    }
  }
  startTimer(namespace) {
    this.timers[namespace] = new Date();
  }
  logTimer(namespace) {
    const elapserdTime = (new Date()).getTime() - this.timers[namespace].getTime();
    console.log(`该函数消耗了${elapserdTime}ms`);
  }
}
// example
class Test {
  constructor() {
    this.name = 'cjg';
    this.age = 22;
  }
  sayName() {
    console.log(this.name);
  }
  sayAge() {
    console.log(this.age);
  }
}

let test1 = new Test();
test1 = new TimeProfileDecorator(test1, ['sayName', 'sayAge']);
console.log(test1.sayName());
console.log(test1.sayAge());

对函数进行增强

节流函数or防抖函数

function throttle(func, delay) {
    const self = this;
    let tid;
    return function(...args) {
        if (tid) return;
        tid = setTimeout(() => {
            func.call(self, ...args);
            tid = null;
        }, delay);
    }
}

function debounce(func, delay) {
    const self = this;
    let tid;
    return function(...args) {
        if (tid) clearTimeout(tid);
        tid = setTimeout(() => {
            func.call(self, ...args);
            tid = null;
        }, delay);
    }
}

缓存函数返回值

// 缓存函数结果,对于一些计算量比较大的函数效果比较明显。
function memorize(func) {
    const cache = {};
    return function (...args) {
        const key = JSON.stringify(args);
        if (cache[key]) {
          console.log('缓存了');
          return cache[key];
        }
        const result = func.call(this, ...args);
        cache[key] = result;
        return result;
    };
}

function fib(num) {
  return num < 2 ? num : fib(num - 1) + fib(num - 2);
}

const enhanceFib = memorize(fib);
console.log(enhanceFib(40));
console.log(enhanceFib(40));
console.log(enhanceFib(40));
console.log(enhanceFib(40));

构造React高阶组件,为组件增加额外的功能,比如为组件提供shallowCompare功能:

import React from 'react';
const { Component } = react;

const ShadowCompareDecorator = (Instance) => class extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return !shallowCompare(this.props, nextProps) ||
      !shallowCompare(this.state, nextState);
  }
  render() {
    return (
      <Instance {...this.props} />
    );
  }
};

export default ShadowCompareDecorator;

当然,你如果用过react-redux的话,你肯定也用过connect。其实connect也是一种高阶组件的方式。它通过装饰者模式,从Provider的context里拿到全局的state,并且将其通过props的方式传给原来的组件。

总结

使用装饰者模式可以让我们为原有的类和函数增添新的功能,并且不会修改原有的代码或者改变其调用方式,因此不会对原有的系统带来副作用。我们也不用担心原来系统会因为它而失灵或者不兼容。就我个人而言,我觉得这是一种特别好用的设计模式。

一个好消息就是,js的装饰器已经加入了es7的草案里啦。它让我们可以更加优雅的使用装饰者模式,如果有兴趣的可以添加下babel的plugins插件提前体验下。阮一峰老师的这个教程也十分浅显易懂。

参考文献:

Javascript设计模式

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员的诗和远方

20180803_ARTS_week06

这个是个比较不好的解法,就是像题目介绍里面那样先把这个『之』字形给做出来,然后再逐行读成字符串,但是通过这个比较好帮助我们理解这个题目。

10010
来自专栏Android 开发者

[译] Kotlin 揭秘:理解并速记 Lambda 语法

在奥地利旅行期间,我参观了维也纳的奥地利国家图书馆。特别是国会大厅,这个令人惊叹的空间感觉就像印第安纳琼斯电影中的一些东西。房间周围的空间是这些门被装在架子上,...

9800
来自专栏xingoo, 一个梦想做发明家的程序员

通过两个小栗子来说说Java的sleep、wait、notify、notifyAll的用法

线程是计算程序运行的最小载体,由于单个单核CPU的硬件水平发展到了一定的瓶颈期,因此就出现了多核多CPU的情况,直接就导致程序员多线程编程的复杂。由此可见线程...

25380
来自专栏mathor

HDOJ1257最少拦截系统

 这个题一开始我百思不得其解,后来看了一眼别人的题解标题,最长上升子序列?我当时还纳闷,这和最长上升子序列有什么关系,后来仔细一想还确实是。因为导弹拦截系统...

10520
来自专栏Java大联盟

23种设计模式详解(三)

10510
来自专栏即时通讯技术

字符编码那点事:快速理解ASCII、Unicode、GBK和UTF-8

原作者:阮一峰(ruanyifeng.com),现重新整理发布,感谢原作者的无私分享。

26520
来自专栏Crossin的编程教室

【每周一坑】求乘积最大

明天就是五一小长假了,又到了换个地方领略祖国大好河山上的人民的时候了,祝大家长假愉快。 ? 在放假之前,利用一点儿闲暇时间,看看本周的题目吧。 设定一个长度为 ...

310100
来自专栏Albert陈凯

2018-11-08 杀死If Else switch case(策略模式+工厂模式+map)套餐 Kill 项目中的switch case

为了便于理解,举个没有业务逻辑的例子,基于这个例子上进行优化。 现在是12:47,举个饭后吃水果的例子哈哈哈(逃 假设我们可以选择的水果有香蕉、西瓜和苹果。吃香...

21930
来自专栏程序员互动联盟

【答疑解惑】为什么这么多人不喜欢用goto?

有网友在群里面问了一个问题:goto语句如何使用?这个问题引发了一系列争论。如下: ? ? ? ? ? 上面的讨论总结下来就是两点,有人建议尽量不要使用got...

35250
来自专栏JAVA高级架构

装饰模式(Decorator)

10410

扫码关注云+社区

领取腾讯云代金券