装饰器的应用场景

按钮是我们平时写业务时常见的页面元素。假设我们的初始需求是:每个业务中的按钮在点击后都弹出「您还未登录哦」的弹框。
那我们可以很轻易地写出这个需求的代码:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>按钮点击需求1.0</title>
  6. </head>
  7. <style>
  8. #modal {
  9. height: 200px;
  10. width: 200px;
  11. line-height: 200px;
  12. position: fixed;
  13. left: 50%;
  14. top: 50%;
  15. transform: translate(-50%, -50%);
  16. border: 1px solid black;
  17. text-align: center;
  18. }
  19. </style>
  20. <body>
  21. <button id='open'>点击打开</button>
  22. <button id='close'>关闭弹框</button>
  23. </body>
  24. <script>
  25. // 弹框创建逻辑,这里我们复用了单例模式面试题的例子
  26. const Modal = (function() {
  27. let modal = null
  28. return function() {
  29. if(!modal) {
  30. modal = document.createElement('div')
  31. modal.innerHTML = '您还未登录哦~'
  32. modal.id = 'modal'
  33. modal.style.display = 'none'
  34. document.body.appendChild(modal)
  35. }
  36. return modal
  37. }
  38. })()
  39. // 点击打开按钮展示模态框
  40. document.getElementById('open').addEventListener('click', function() {
  41. // 未点击则不创建modal实例,避免不必要的内存占用
  42. const modal = new Modal()
  43. modal.style.display = 'block'
  44. })
  45. // 点击关闭按钮隐藏模态框
  46. document.getElementById('close').addEventListener('click', function() {
  47. const modal = document.getElementById('modal')
  48. if(modal) {
  49. modal.style.display = 'none'
  50. }
  51. })
  52. </script>
  53. </html>

按钮发布上线后,过了几天太平日子。忽然有一天,产品经理找到你,说这个弹框提示还不够明显,我们应该在弹框被关闭后把按钮的文案改为“快去登录”,同时把按钮置灰。
听到这个消息,你立刻马不停蹄地翻出之前的代码,找到了按钮的 click 监听函数,手动往里面添加了文案修改&按钮置灰逻辑。但这还没完,因为你司的几乎每个业务里都用到了这类按钮:除了“点击打开”按钮,还有“点我开始”、“点击购买”按钮等各种五花八门的按钮,这意味着你不得不深入到每一个业务的深处去给不同的按钮添加这部分逻辑。
有的业务不在你这儿,但作为这个新功能迭代的 owner,你还需要把需求细节再通知到每一个相关同事(要么你就自己上,去改别人的代码,更恐怖),怎么想怎么麻烦。一个文案修改&按钮置灰尚且如此麻烦,更不要说我们日常开发中遇到的更复杂的需求变更了。
不仅麻烦,直接去修改已有的函数体,这种做法违背了我们的“开放封闭原则”;往一个函数体里塞这么多逻辑,违背了我们的“单一职责原则”。所以说这个事儿,越想越不能这么干。
我想一定会有同学质疑说为啥不把按钮抽成公共组件 Button,这样只需要在 Button 组件里修改一次逻辑就可以了。这种想法非常好。但注意,我们楼上的例子没有写组件直接写了 Button 标签是为了简化示例。事实上真要写组件的话,不同业务里必定有针对业务定制的不同 Button 组件,比如 MoreButton 、BeginButton等等,也是五花八门的,所以说我们仍会遇到同样的困境。
讲真,我想任何人去做这个需求的时候,其实都压根不想去关心它现有的业务逻辑是啥样的——你说这按钮的旧逻辑是我自己写的还好,理解成本不高;万一碰上是个离职同事写的,那阅读难度谁能预料呢?我不想接锅,我只是想对它已有的功能做个拓展,只关心拓展出来的那部分新功能如何实现,对不对?
程序员说:“我不想努力了,我想开挂”,于是便有了装饰器模式。

装饰器模式初相见

为了不被已有的业务逻辑干扰,当务之急就是将旧逻辑与新逻辑分离,把旧逻辑抽出去

  1. // 将展示Modal的逻辑单独封装
  2. function openModal() {
  3. const modal = new Modal()
  4. modal.style.display = 'block'
  5. }

编写新逻辑:

  1. // 按钮文案修改逻辑
  2. function changeButtonText() {
  3. const btn = document.getElementById('open')
  4. btn.innerText = '快去登录'
  5. }
  6. // 按钮置灰逻辑
  7. function disableButton() {
  8. const btn = document.getElementById('open')
  9. btn.setAttribute("disabled", true)
  10. }
  11. // 新版本功能逻辑整合
  12. function changeButtonStatus() {
  13. changeButtonText()
  14. disableButton()
  15. }

然后把三个操作逐个添加open按钮的监听函数里:

  1. document.getElementById('open').addEventListener('click', function() {
  2. openModal()
  3. changeButtonStatus()
  4. })

如此一来,我们就实现了“只添加,不修改”的装饰器模式,使用changeButtonStatus的逻辑装饰了旧的按钮点击逻辑。以上是ES5中的实现,ES6中,我们可以以一种更加面向对象化的方式去写:

  1. // 定义打开按钮
  2. class OpenButton {
  3. // 点击后展示弹框(旧逻辑)
  4. onClick() {
  5. const modal = new Modal()
  6. modal.style.display = 'block'
  7. }
  8. }
  9. // 定义按钮对应的装饰器
  10. class Decorator {
  11. // 将按钮实例传入
  12. constructor(open_button) {
  13. this.open_button = open_button
  14. }
  15. onClick() {
  16. this.open_button.onClick()
  17. // “包装”了一层新逻辑
  18. this.changeButtonStatus()
  19. }
  20. changeButtonStatus() {
  21. this.changeButtonText()
  22. this.disableButton()
  23. }
  24. disableButton() {
  25. const btn = document.getElementById('open')
  26. btn.setAttribute("disabled", true)
  27. }
  28. changeButtonText() {
  29. const btn = document.getElementById('open')
  30. btn.innerText = '快去登录'
  31. }
  32. }
  33. const openButton = new OpenButton()
  34. const decorator = new Decorator(openButton)
  35. document.getElementById('open').addEventListener('click', function() {
  36. // openButton.onClick()
  37. // 此处可以分别尝试两个实例的onClick方法,验证装饰器是否生效
  38. decorator.onClick()
  39. })

大家这里需要特别关注一下 ES6 这个版本的实现,这里我们把按钮实例传给了 Decorator,以便于后续 Decorator 可以对它为所欲为进行逻辑的拓展。在 ES7 中,Decorator 作为一种语法被直接支持了,它的书写会变得更加简单,但背后的原理其实与此大同小异。在下一节,我们将一起去探究一下 ES7 中 Decorator 背后的故事。

值得关注的细节

结束了装饰器的感性认知之旅,下一节我们将直奔ES7装饰器原理&装饰器优秀案例教学。在此之前,我们对本节的一个小细节进行复盘:

单一职责原则

大家可能刚刚没来得及注意,按钮新逻辑中,文本修改&按钮置灰这两个变化,被我封装在了两个不同的方法里,并以组合的形式出现在了最终的目标方法changeButtonStatus里。这样做的目的是为了强化大家脑中的“单一职责”意识。将不同的职责分离,可以做到每个职责都能被灵活地复用;同时,不同职责之间无法相互干扰,不会出现因为修改了 A 逻辑而影响了 B 逻辑的狗血剧情。