经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » JavaScript » 查看文章
简单易懂的设计模式(上)
来源:cnblogs  作者:凹凸实验室  时间:2021/6/21 10:06:40  对本文有异议

一、单例模式

1. 什么是单例模式

单例模式的定义是,保证一个类仅有一个实例,并提供一个访问它的全局访问点。

有一些对象,比如线程池/全局缓存/浏览器中的 window 对象等等,我们就只需要一个实例。

下面将根据实际场景进行介绍。

2. 实际场景

1. 登录浮窗

当我们单击登录按钮时,页面中会出现一个登录的浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。

1.1 传统做法

传统做法在页面加载完成时,就创建好登录浮窗,当用户点击登录按钮时,显示登录浮窗,实现代码如下:

  1. <button id="loginBtn">登录</button>
  1. var loginLayer = (() => {
  2. let div = document.createElement('div')
  3. div.innerHTML = '我是登录弹窗'
  4. div.style.display = 'none'
  5. document.body.appendChild(div)
  6. return div
  7. })()
  8. document.getElementById('loginBtn').onclick = () => {
  9. loginLayer.style.display = 'block'
  10. }

上述代码有以下缺点:

  1. 在无需登录的情况下,也会新增登录浮窗的 DOM 节点,浪费性能。

现在优化一下,将代码改为,在用户点击登录按钮后,才新增登录浮窗的 DOM 节点。

代码如下:

  1. var createLoginLayer = () => {
  2. let div = document.createElement('div')
  3. div.innerHTML = '我是登录弹窗'
  4. div.style.display = 'none'
  5. document.body.appendChild(div)
  6. return div
  7. }
  8. document.getElementById('loginBtn').onclick = () => {
  9. const loginLayer = createLoginLayer()
  10. loginLayer.style.display = 'block'
  11. }

上述代码也存在缺陷,具体如下:

  1. 每次点击登录按钮,都会创建一个登录浮窗,频繁的创建 DOM 节点更加浪费性能。

实际上,我们只需要创建一次登录浮窗。

1.2 单例模式

通过单例模式,重构上述代码。

  1. const createLoginLayer = () => {
  2. const div = document.createElement('div')
  3. div.innerHTML = '我是登录弹窗'
  4. div.style.display = 'none'
  5. console.log(123)
  6. document.body.appendChild(div)
  7. return div
  8. }
  9. const createSingle = (function () {
  10. var instance = {}
  11. return function (fn) {
  12. if (!instance[fn.name]) {
  13. instance[fn.name] = fn.apply(this, arguments)
  14. }
  15. return instance[fn.name]
  16. }
  17. })()
  18. const createIframe = function () {
  19. const iframe = document.createElement('iframe')
  20. document.body.appendChild(iframe)
  21. iframe.style.display = 'none'
  22. return iframe
  23. }
  24. const createSingleLoginLayer = createSingle(createLoginLayer)
  25. const createSingleIframe = createSingle(createIframe)
  26. document.getElementById('loginBtn').onclick = () => {
  27. const loginLayer = createSingleLoginLayer
  28. const iframe = createSingleIframe
  29. loginLayer.style.display = 'block'
  30. iframe.style.display = 'block'
  31. }

经过重构,代码做了以下优化:

  1. 将创建实例对象 createLoginLayer / createIframe 的职责和管理单例对象 createSingle 的职责分离,符合单一职责原则;
  2. 通过闭包存储实例,并进行判断,不管点击登录按钮多少次,只创建一个登录浮窗实例
  3. 易于扩展,当下次需要创建页面中唯一的 iframe / script 等其他标签时,可以直接复用该逻辑。

3. 总结

单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。

二、策略模式

1. 什么是策略模式

当我们计划国庆出去游玩时,在交通方式上,我们可以选择贵而快的飞机、价格中等但稍慢的动车、便宜但超级慢的火车,根据不同的人,选择对应的交通方式,且可以随意更换交通方式,这就是策略模式

策略模式的定义是,定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。

2. 实际场景

1. 计算年终奖

1.1 传统做法

有一个计算员工年终奖的需求,假设,绩效为 S 的员工年终奖是 4 倍工资,绩效为 A 的员工年终奖是 3 倍工资,绩效为 B 的员工年终奖是 2 倍工资,下面我们来计算员工的年终奖。

  1. var calculateBonus = function(performanceLevel, salary) {
  2. if (performanceLevel === 'S') {
  3. return salary * 4;
  4. }
  5. if (performanceLevel === 'A') {
  6. return salary * 3;
  7. }
  8. if (performanceLevel === 'B') {
  9. return salary * 2;
  10. }
  11. };
  12. calculateBonus('B', 20000); // 输出:40000
  13. calculateBonus( 'S', 6000 ); // 输出:24000

上述代码有以下缺点:

  1. 使用 if-else 语句描述逻辑,代码庞大;
  2. 缺乏弹性,如果需要修改绩效 S 的奖金系数,必须修改 calculateBonus 函数,违反了开放-封闭原则;
  3. 无法再次复用,当其他地方需要用到这套逻辑,只能再复制一份。

1.2 策略模式做法

使用策略模式改良后

  1. const strategies = {
  2. S: salary => {
  3. return salary * 4
  4. },
  5. A: salary => {
  6. return salary * 3
  7. },
  8. B: salary => {
  9. return salary * 2
  10. }
  11. }
  12. const calculateBonus = (level, salary) => {
  13. return strtegies[level](salary)
  14. }
  15. console.log(calculateBonus('s', 20000))
  16. console.log(calculateBonus('a', 10000))

可以看到上述代码做了以下改动:

  1. 策略类 strategies 封装了具体的算法和计算过程(每种绩效的计算规则);
  2. 环境类 calculateBonus 接受请求,把请求委托给策略类 strategies(员工的绩效和工资;
  3. 将算法的使用和算法的实现分离,代码清晰,职责分明;
  4. 消除大量的 if-else 语句。

1.3 小结

策略模式使代码可读性更高,易于拓展更多的策略算法。当绩效系数改变,或者绩效等级增加,我们只需要为 strategies 调整或新增算法,符合开放-封闭原则。

2. 表单校验

当网页上的表单需要校验输入框/复选框等等规则时,如何去实现呢?

现在有一个注册用户的表单需求,在提交表单之前,需要验证以下规则:

  1. 用户名不能为空
  2. 密码长度不能少于 6 位
  3. 手机号码必须符合格式

2.1 传统做法

使用 if-else 语句判断表单输入是否符合对应规则,如不符合,提示错误原因。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title></title>
  5. </head>
  6. <body>
  7. <form id='registerForm' action="xxx" method="post">
  8. 用户名:<input type="text" name="userName">
  9. 密码:<input type="text" name="password">
  10. 手机号:<input type="text" name="phone">
  11. <button>提交</button>
  12. </form>
  13. <script type="text/javascript">
  14. let registerForm = document.getElementById('registerForm')
  15. registerForm.onsubmit = () => {
  16. if (registerForm.userName.value) {
  17. alert('用户名不能为空')
  18. return false
  19. }
  20. if (registerForm.password.value.length < 6) {
  21. alert('密码长度不能少于6')
  22. return false
  23. }
  24. if (!/(^1[3|5|8][0-9]$)/.test(registerForm.phone.value)) {
  25. alert('手机号码格式不正确')
  26. return false
  27. }
  28. }
  29. </script>
  30. </body>
  31. </html>

image.png

上述代码有以下缺点:

  • onsubmit 函数庞大,包含大量 if-else 语句;
  • onsubmit 缺乏弹性,当有规则需要调整,或者需要新增规则时,需要改动 onsubmit 函数内部,违反开放-封闭原则;
  • 算法复用性差,只能通过复制,复用到其他表单。

2.2 策略模式做法

使用策略模式重构上述代码。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title></title>
  5. </head>
  6. <body>
  7. <form action="http://xxx.com/register" id="registerForm" method="post">
  8. 请输入用户名:
  9. <input type="text" name="userName" />
  10. 请输入密码:
  11. <input type="text" name="password" />
  12. 请输入手机号码:
  13. <input type="text" name="phoneNumber" />
  14. <button>
  15. 提交
  16. </button>
  17. </form>
  18. <script type="text/javascript" src="index.js">
  19. </script>
  20. </body>
  21. </html>
  1. // 表单dom
  2. const registerForm = document.getElementById('registerForm')
  3. // 表单规则
  4. const rules = {
  5. userName: [
  6. {
  7. strategy: 'isNonEmpty',
  8. errorMsg: '用户名不能为空'
  9. },
  10. {
  11. strategy: 'minLength:10',
  12. errorMsg: '用户名长度不能小于10位'
  13. }
  14. ],
  15. password: [
  16. {
  17. strategy: 'minLength:6',
  18. errorMsg: '密码长度不能小于6位'
  19. }
  20. ],
  21. phoneNumber: [
  22. {
  23. strategy: 'isMobile',
  24. errorMsg: '手机号码格式不正确'
  25. }
  26. ]
  27. }
  28. // 策略类
  29. var strategies = {
  30. isNonEmpty: function(value, errorMsg) {
  31. if (value === '') {
  32. return errorMsg;
  33. }
  34. },
  35. minLength: function(value, errorMsg, length) {
  36. console.log(length)
  37. if (value.length < length) {
  38. return errorMsg;
  39. }
  40. },
  41. isMobile: function(value, errorMsg) {
  42. if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
  43. return errorMsg;
  44. }
  45. }
  46. };
  47. // 验证类
  48. const Validator = function () {
  49. this.cache = []
  50. }
  51. // 添加验证方法
  52. Validator.prototype.add = function ({ dom, rules}) {
  53. rules.forEach(rule => {
  54. const { strategy, errorMsg } = rule
  55. console.log(rule)
  56. const [ strategyName, strategyCondition ] = strategy.split(':')
  57. console.log(strategyName)
  58. const { value } = dom
  59. this.cache.push(strategies[strategyName].bind(dom, value, errorMsg, strategyCondition))
  60. })
  61. }
  62. // 开始验证
  63. Validator.prototype.start = function () {
  64. let errorMsg
  65. this.cache.some(cacheItem => {
  66. const _errorMsg = cacheItem()
  67. if (_errorMsg) {
  68. errorMsg = _errorMsg
  69. return true
  70. } else {
  71. return false
  72. }
  73. })
  74. return errorMsg
  75. }
  76. // 验证函数
  77. const validatorFn = () => {
  78. const validator = new Validator()
  79. console.log(validator.add)
  80. Object.keys(rules).forEach(key => {
  81. console.log(2222222, rules[key])
  82. validator.add({
  83. dom: registerForm[key],
  84. rules: rules[key]
  85. })
  86. })
  87. const errorMsg = validator.start()
  88. return errorMsg
  89. }
  90. // 表单提交
  91. registerForm.onsubmit = () => {
  92. const errorMsg = validatorFn()
  93. if (errorMsg) {
  94. alert(errorMsg)
  95. return false
  96. }
  97. return false
  98. }

上述代码通过 strategies 定义规则算法,通过 Validator 定义验证算法,将规则和算法分离,我们仅仅通过配置的方式就可以完成表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便的被移植到其他项目中。

3. 总结

策略模式是一种常用且有效的设计模式,通过上述例子,可以总结出策略模式的一些优点:

  • 策略模式利用组合/委托和多态等技术和思想,可以有效的避免多重条件选择语句;
  • 策略模式提供了对开放-封闭原则的完美支持,将算法封装中独立的策略类中,使得它们易于切换/理解/扩展;
  • 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的代替方案。

三、代理模式

1. 什么是代理模式

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。

2. 模拟场景

1. 小明送花给小白

1.1 传统做法

传统做法是小明直接把花送给小白,小白接收到花,代码如下:

  1. const Flower = function () {
  2. return '玫瑰??'
  3. }
  4. const xiaoming = {
  5. sendFlower: target => {
  6. const flower = new Flower()
  7. target.receiveFlower(flower)
  8. }
  9. }
  10. const xiaobai = {
  11. receiveFlower: flower => {
  12. console.log('收到花', flower)
  13. }
  14. }
  15. xiaoming.sendFlower(xiaobai)

1.2 代理模式

但是,小明并不认识小白,他想要通过小代,帮他打探小白的情况,在小白心情好的时候送花,这样成功率更高。代码如下:

  1. const Flower = function () {
  2. return '玫瑰??'
  3. }
  4. const xiaoming = {
  5. sendFlower: target => {
  6. const flower = new Flower()
  7. target.receiveFlower(flower)
  8. }
  9. }
  10. const xiaodai = {
  11. receiveFlower: flower => {
  12. xiaobai.listenGoodMood().then(() => {
  13. xiaobai.receiveFlower(flower)
  14. })
  15. }
  16. }
  17. const xiaobai = {
  18. receiveFlower: flower => {
  19. console.log('收到花', flower)
  20. },
  21. listenGoodMood: fn => {
  22. return new Promise((reslove, reject) => {
  23. // 10秒后,心情变好
  24. reslove()
  25. })
  26. }
  27. }
  28. xiaoming.sendFlower(xiaodai)

以上,小明通过小代,监听到小白心情的心情变化,选择在小白心情好时送花给小白。不仅如此,小代还可以做以下事情:

  1. 帮助小白过滤掉一些送花的请求,这就叫做保护代理;
  2. 帮助小明,在小白心情好时,再执行买花操作,这就叫做虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。

3. 实际场景

1. 图片预加载

图片预加载时一种常见的技术,如果直接给 img 标签节点设置 src 属性,由于图片过大或网络不佳,图片的位置往往有一段时间时空白。

1.1 传统做法

  1. const myImage = (() => {
  2. const imgNode = document.createElement('img')
  3. document.body.appendChild(imgNode)
  4. return {
  5. setSrc: src => {
  6. imgNode.src = src
  7. }
  8. }
  9. })()
  10. myImage.setSrc('https://img30.360buyimg.com/ling/jfs/t1/187775/5/8271/435193/60c8117eE7d79ef41/1d21db2c4dca9a90.png')

通过开发者工具把网速设置为 5kb/s 时,会发现在很长一段时间内,图片位置是空白的。

image.png

1.2 虚拟代理

下面用虚拟代理优化该功能,把加载图片的操作交给代理函数完成,在图片加载时,先用一张loading 图占位,当图片加载成功后,再把它填充进 img 节点。

代码如下:

  1. const myImage = (() => {
  2. const imgNode = document.createElement('img')
  3. document.body.appendChild(imgNode)
  4. return {
  5. setSrc: src => {
  6. imgNode.src = src
  7. }
  8. }
  9. })()
  10. const loadingSrc = '../../../../img/loading.gif'
  11. const imgSrc = 'https://img30.360buyimg.com/ling/jfs/t1/187775/5/8271/435193/60c8117eE7d79ef41/1d21db2c4dca9a90.png'
  12. const proxyImage = (function () {
  13. const img = new Image()
  14. img.onload = () => {
  15. myImage.setSrc(img.src)
  16. }
  17. return {
  18. setSrc: src => {
  19. myImage.setSrc(loadingSrc)
  20. img.src = src
  21. }
  22. }
  23. })()
  24. proxyImage.setSrc(imgSrc)

上述代码有以下优点:

  1. 通过 proxyImage 控制了对 MyImage 的访问,在 MyImage 未加载成功之前,使用 loading 图占位;

  2. 践行单一职责原则,给 img 节点设置 src 的函数 MyImage,预加载图片的函数 proxyImage,都只有一个职责;

  3. 践行开放-封闭原则,给 img 节点设置 src 和预加载图片的功能,被隔离在两个对象里,它们可以各自变化不影响对方。

2. 合并HTTP请求

假设我们要实现一个同步文件的功能,通过复选框,当复选框选中的时候,将该复选框对应的 id 传给服务器,告诉服务器需要同步 id 对应的文件。

思考一下,会发现,如果每选中一个复选框,就请求一次接口,假设 1s 内选中了 10 个复选框,那么就要发送 10 次请求。

2.1 虚拟代理

可以通过虚拟代理来优化上述做法,新增一个代理,帮助复选框发起同步文件的请求,收集在这 1s 内的请求,1s 后再一起把这些文件 id 发送到服务器。

代码如下:

  1. <!DOCTYPE html>
  2. <html>
  3. <meta charset="utf-8" />
  4. <head>
  5. <title></title>
  6. </head>
  7. <body>
  8. a <input type="checkbox" value="a" />
  9. b <input type="checkbox" value="b" />
  10. c <input type="checkbox" value="c" />
  11. d <input type="checkbox" value="d" />
  12. <script type="text/javascript" src="index.js">
  13. </script>
  14. </body>
  15. </html>
  1. const synchronousFile = cache => {
  2. console.log('开始同步文件,id为:'+ cache.join('/'))
  3. }
  4. const proxySynchronousFile = (() => {
  5. const cache = []
  6. let timer
  7. return id => {
  8. console.log(id)
  9. cache.push(id)
  10. if (timer) {
  11. return
  12. }
  13. timer = setTimeout(() => {
  14. synchronousFile(cache)
  15. clearTimeout(timer)
  16. timer = null
  17. cache.length = 0
  18. }, 2000)
  19. }
  20. })()
  21. const checkbox = document.getElementsByTagName('input')
  22. Array.from(checkbox).forEach(i => {
  23. console.log(i)
  24. i.onclick = () => {
  25. if (i.checked) {
  26. proxySynchronousFile(i.value)
  27. }
  28. }
  29. })

3. ajax异步请求数据

在列表需要分页时,同一页的数据理论上只需要去后台拉取一次,可以把这些拉取过的数据缓存下来,下次请求时直接使用缓存数据。

3.1 缓存代理

使用缓存代理实现上述功能,代码如下:

  1. (async function () {
  2. function getArticle (currentPage, pageSize) {
  3. console.log('getArticle', currentPage, pageSize)
  4. // 模拟一个ajax请求
  5. return new Promise((resolve, reject) => {
  6. resolve({
  7. ok: true,
  8. data: {
  9. list: [],
  10. total: 10,
  11. params: {
  12. currentPage,
  13. pageSize
  14. }
  15. }
  16. })
  17. })
  18. }
  19. const proxyGetArticle = (() => {
  20. const caches = []
  21. return async (currentPage, pageSize) => {
  22. const cache = Array.prototype.join.call([currentPage, pageSize],',')
  23. if (cache in caches) {
  24. return caches[cache]
  25. }
  26. const { data, ok } = await getArticle(currentPage, pageSize)
  27. if (ok) {
  28. caches[cache] = data
  29. }
  30. return caches[cache]
  31. }
  32. })()
  33. // 搜索第一页
  34. await proxyGetArticle(1, 10)
  35. // 搜索第二页
  36. await proxyGetArticle(2, 10)
  37. // 再次搜索第一页
  38. await proxyGetArticle(1, 10)
  39. })()

通过缓存代理,在第二次请求第一页的数据时,直接在缓存数据中拉取,无须再次从服务器请求数据。

4. 总结

上面根据实际场景介绍了虚拟代理和缓存代理的做法。

当我们不方便直接访问某个对象时,找一个代理方法帮我们去访问该对象,这就是代理模式。

可通过 github源码 进行实操练习。

希望本文能对你有所帮助,感谢阅读??~


欢迎关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章:

欢迎关注凹凸实验室公众号

原文链接:http://www.cnblogs.com/o2team/p/14899040.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号