# 用装饰器模式解决频繁修改的需求

目的

装饰器模式(Decorator Pattern) 的目的非常简单,那就是:

在不修改原有代码的情况下增加逻辑。

这句话听起来可能有些矛盾,既然都要增加逻辑了,怎么可能不去修改原有的代码?但 SOLID (向对象设计5大重要原则)的开放封闭原则就是在试图解决这个问题,其内容是不去改动已经写好的核心逻辑,但又能够扩充新逻辑,也就是对扩展开放,对修改关闭。

举个例子,我们想要实现一个专门在浏览器的控制台中输出文本的功能,可能会这样做:

class Printer {
  print(text) {
    console.log(text);
  }
}

const printer = new Printer();
printer.print('something'); // something

在你满意的看着自己的成果时,产品过来说了一句:“我觉得颜色不够突出,还是把它改成黄色的吧!”

小菜一碟!你自信的打开百度一通操作之后,把代码改成了下面这样子:

class Printer {
  print(text) {
    console.log(`%c${text}`,'color: yellow;');
  }
}

image.png

但产品看了看又说:“这个字体有点太小了,再大一点,最好是高端大气上档次那种。

”好吧。。。“你强行控制着自己拿刀的冲动,一边琢磨多大的字体才是高端大气上档次,一边修改 print 的代码:

class Printer {
  print(text) {
    console.log(`%c${text}`,'color: yellow;font-size: 36px;');
  }
}

image.png

这次改完你之后你心中已经满是 mmp 了,而且在不停的想:

“这样真的好吗?”

你无法保证这次是最后的修改,而且也可能会不只一个产品来对你指手划脚。你呆呆的看着显示器,直到电脑进入休眠模式,屏幕中映出你那张苦大仇深的脸,想着不断变得乱七八糟的 print 方法,不知道该怎么去应付那些永无休止的需求。。。

在上面的例子中,最开始的 Printer 按照需求写出它应该要有的逻辑,那就是在控制台中输出一些文本。换句话说,当写完“在 console 中输出一些文本”这段逻辑后,就能将 Printer 结束了,因为它就是 Printer 的全部了。那在这个情况下该如何改变字体或是颜色的逻辑呢?

这时你该需要装饰器模式了。

Decorator Pattern(装饰器模式)

首先修改原来的 Printer,使它可以支持扩充样式:

class Printer {
  print(text = '', style = '') {
    console.log(`%c${text}`, style);
  }
}

之后分别创建改变字体和颜色的装饰器:

const yellowStyle = (printer) => ({
  ...printer,
  print: (text = '', style = '') => {
    printer.print(text, `${style}color: yellow;`);
  }
});

const boldStyle = (printer) => ({
  ...printer,
  print: (text = '', style = '') => {
    printer.print(text, `${style}font-weight: bold;`);
  }
});

const bigSizeStyle = (printer) => ({
  ...printer,
  print: (text = '', style = '') => {
    printer.print(text, `${style}font-size: 36px;`);
  }
});

代码中的 yellowStyleboldStylebigSizeStyle 分别是给 print 方法的装饰器,它们都会接收 printer,并以 printer 为基础复制出一个一样的对象出来并返回,而返回的 printer 与原来的区别是,各自 Decorator 都会为 printerprint 方法加上各自装饰的逻辑(例如改变字体、颜色或字号)后再调用 printerprint

使用方式如下:

image.png

只要把所有装饰的逻辑抽出来,就能够自由的搭配什么时候要输出什么样式,加入要再增加一个斜体样式,也只需要再新增一个装饰器就行了,不需要改动原来的 print 逻辑。

不过要注意的是上面的代码只是简单的把 Object 用解构复制,如果在 prototype 上存在方法就有可能会出错,所以要深拷贝一个新对象的话,还需要另外编写逻辑:

const copyObj = (originObj) => {
  const originPrototype = Object.getPrototypeOf(originObj);
  let newObj = Object.create(originPrototype);
   
  const originObjOwnProperties = Object.getOwnPropertyNames(originObj);
  originObjOwnProperties.forEach((property) => {
    const prototypeDesc = Object.getOwnPropertyDescriptor(originObj, property);
     Object.defineProperty(newObj, property, prototypeDesc);
  });
  
  return newObj;
}

然后装饰器内改使上面代码中的 copyObj,就能正确复制相同的对象了:


const yellowStyle = (printer) => {
  const decorator = copyObj(printer);

  decorator.print = (text = '', style = '') => {
    printer.print(text, `${style}color: yellow;`);
  };

  return decorator;
};

其他案例

因为我们用的语言是 JavaScript,所以没有用到类,只是简单的装饰某个方法,比如下面这个用来发布文章的 publishArticle

const publishArticle = () => {
  console.log('发布文章');
};

如果你想要再发布文章之后在 微博或QQ空间之类的平台上发个动态,那又该怎么处理呢?是像下面的代码这样吗?

const publishArticle = () => {
  console.log('发布文章');

  console.log('发 微博 动态');
  console.log('发 QQ空间 动态');
};

这样显然不好!publishArticle 应该只需要发布文章的逻辑就够了!而且如果之后第三方服务平台越来越多,那 publishArticle 就会陷入一直加逻辑一直爽的情况,在明白了装饰器模式后就不能再这样做了!

所以把这个需求套上装饰器:

const publishArticle = () => {
  console.log('发布文章');
};

const publishWeibo = (publish) => (...args) => {
  publish(args);
  console.log('发 微博 动态');
};

const publishQzone = (publish) => (...args) => {
  publish(args);
  console.log('发 QQ空间 动态');
};


const publishArticleAndWeiboAndQzone = publishWeibo(publishQzone(publishArticle));

前面 Printer 的例子是复制一个对象并返回,但如果是方法就不用复制了,只要确保每个装饰器都会返回一个新方法,然后会去执行被装饰的方法就行了。

总结

装饰器模式是一种非常有用的设计模式,在项目中也会经常用到,当需求变动时,觉得某个逻辑很多余,那么直接不装饰它就行了,也不需要去修改实现逻辑的代码。每一个装饰器都做他自己的事情,与其他装饰器互不影响。

173382ede7319973.gif


本文首发微信公众号:前端先锋
欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:



疯狂的技术宅
本专栏文章首发于公众号:前端先锋 。
44.3k 声望
39.1k 粉丝
0 条评论
推荐阅读
为什么浮点数运算会产生误差
以下图的 Python 为例, 0.1 + 0.2 并不等于 0.3,8.7 / 10 也不等于 0.87,而是 0.869999…,真是太奇怪了 🤔

疯狂的技术宅12阅读 7.4k评论 1

正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青57阅读 8.7k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy49阅读 7.4k评论 12

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs42阅读 7k评论 12

封面图
CSS 绘制一只思否猫
欢迎关注我的公众号:前端侦探练习 CSS 有一个比较有趣的方式,就是发挥想象,绘制各式各样的图案,比如来绘制一只思否猫?思否猫,SegmentFault 思否的吉祥物,是一只独一无二、特立独行、热爱自由的(>^ω^&lt...

XboxYan48阅读 3.4k评论 14

封面图
「多图预警」完美实现一个@功能
一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala...(内心 OS:不听,讲重点)产品大大:咱们的客服 I...

wuwhs32阅读 3.6k评论 5

封面图
还在用 JS 做节流吗?CSS 也可以防止按钮重复点击
举个例子:一个保存按钮,为了避免重复提交或者服务器考虑,往往需要对点击行为做一定的限制,比如只允许每300ms提交一次,这时候我想大部分同学都会到网上直接拷贝一段throttle函数,或者直接引用lodash工具库

XboxYan35阅读 2.7k评论 2

封面图
44.3k 声望
39.1k 粉丝
宣传栏