目的
装饰器模式(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;');
}
}
但产品看了看又说:“这个字体有点太小了,再大一点,最好是高端大气上档次那种。
”好吧。。。“你强行控制着自己拿刀的冲动,一边琢磨多大的字体才是高端大气上档次,一边修改 print
的代码:
class Printer {
print(text) {
console.log(`%c${text}`,'color: yellow;font-size: 36px;');
}
}
这次改完你之后你心中已经满是 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;`);
}
});
代码中的 yellowStyle
、boldStyle
和 bigSizeStyle
分别是给 print
方法的装饰器,它们都会接收 printer
,并以 printer
为基础复制出一个一样的对象出来并返回,而返回的 printer
与原来的区别是,各自 Decorator
都会为 printer
的 print
方法加上各自装饰的逻辑(例如改变字体、颜色或字号)后再调用 printer
的 print
。
使用方式如下:
只要把所有装饰的逻辑抽出来,就能够自由的搭配什么时候要输出什么样式,加入要再增加一个斜体样式,也只需要再新增一个装饰器就行了,不需要改动原来的 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
的例子是复制一个对象并返回,但如果是方法就不用复制了,只要确保每个装饰器都会返回一个新方法,然后会去执行被装饰的方法就行了。
总结
装饰器模式是一种非常有用的设计模式,在项目中也会经常用到,当需求变动时,觉得某个逻辑很多余,那么直接不装饰它就行了,也不需要去修改实现逻辑的代码。每一个装饰器都做他自己的事情,与其他装饰器互不影响。
本文首发微信公众号:前端先锋
欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章
欢迎继续阅读本专栏其它高赞文章:
- 深入理解Shadow DOM v1
- 一步步教你用 WebVR 实现虚拟现实游戏
- 13个帮你提高开发效率的现代CSS框架
- 快速上手BootstrapVue
- JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的一切
- WebSocket实战:在 Node 和 React 之间进行实时通信
- 关于 Git 的 20 个面试题
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什么?
- 30分钟用Node.js构建一个API服务器
- Javascript的对象拷贝
- 程序员30岁前月薪达不到30K,该何去何从
- 14个最好的 JavaScript 数据可视化库
- 8 个给前端的顶级 VS Code 扩展插件
- Node.js 多线程完全指南
- 把HTML转成PDF的4个方案及实现
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。