理解前端设计模式(一)

602 阅读8分钟

JavaScript设计模式与开发实践 @曾探著 --整理笔记

设计模式的定义是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。

通俗一点说,设计模式就是给面向对象软件开发中的一些好的设计取个名字。这些“好的设计”并不是谁发明的,而是早已存在于软件开发中。一个稍有经验的程序员也许在不知不觉中数次使用过这些设计模式。

我认为学习设计模式的意义在于,理解前辈优秀的编程思维,遇到问题时有更多的思路。

设计原则

为了便于理解模式中提到的设计原则,将原则介绍放在前面

1 单一职责原则(SRP)

一个对象(方法)只做一件事

  一个方法承担了过多的职责,在需求变迁过程中需要改写这个方法的可能性越大,而修改其中一个职责时可能会影响其他功能,使得这种设计变得脆弱并难以维护

  但是如果两个职责总是同时变化,就没必要分离它们,比如创建xhr和发送xhr请求总是一起发生的,这种情况就没必要分离

  另一方面,若自己仅是封装函数的使用者,在使用者的角度会希望把一组相关行为放到一起,因为这样用起来很方便,比如jquery的attr方法,既可以赋值,又可以取值,明显违反了单一职责原则,对封装函数的维护者会带来些困难。这是方便和稳定的取舍

2 开放-封闭原则(OCP)

模块、函数等应该是可以拓展的,但是不可修改

当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。

  大家应该都听过‘用大电风扇吹走生产线产生的空肥皂盒’的故事,因为改造源代码的代价往往会很大。一个几百行的模块,逻辑复杂,改造很容易出问题。需要注意的是,对模块的维护者来说,能否不改造源代码就实现需求变更或新增,很大程度取决于源代码设计是否符合开放-封闭原则。

要设计出这样的程序有一条核心规律:找出可变的部分并把它们封装起来,下面是几种具体实现:

  • 利用对象的多态性改造条件分支语句
    • 每当需要增加一个新的if语句,都要被迫修改原函数,实际上我们可以利用对象的多态性来重构它们
  • 放置挂钩(hook)
    • 在程序有可能出现变化的地方放置一个挂钩,通过其返回结果决定程序的下一步走向,这样就在原来的代码执行路径上出现了一个分叉路口
    • 看到hook就想到React的hooks,想到开始的函数组件只做展示组件用(稳定),hooks的出现令它有了状态和生命周期(可变),我好想明白了什么,又感觉没什么收获~ 待考证
  • 使用回调函数
    • 将易于变化的逻辑封装在回调函数里,然后作为参数传入一个稳定封闭的函数中,此时回调函数就是一种特殊的挂钩

设计模式

1 单例模式

保证仅有一个实例,并提供一个访问它的全局访问点

1.1 示例

如果我们希望在页面中惰性创建一个登录框,点登录时创建,再次点击不重复创建,代码如下:

// 封装一个通用的实现惰性单例的函数
var getSingle = function( fn ){
 var result;
 return function(){
 return result || ( result = fn .apply(this, arguments ) );
 }
}; 
// 生成需要的模块,这里以登录框为例
var createLoginLayer = function(){
 var div = document.createElement( 'div' );
 div.innerHTML = '我是登录浮窗';
 div.style.display = 'none';
 document.body.appendChild( div );
 return div;
}; 
// 调用单例函数给功能模块添加单例特性
var createSingleLoginLayer = getSingle( createLoginLayer );
// 业务逻辑实现
document.getElementById( 'loginBtn' ).onclick = function(){
 var loginLayer = createSingleLoginLayer();
 loginLayer.style.display = 'block';
}; 
  • 将单例特性从具体功能模块中剥离出去,符合‘单一职责原则’,提高代码复用
  • 这个功能令我想起了React的hooks也是类似的效果,是否使用的单例模式待考证

1.2 全局变量

全局变量满足单例模式的两个条件(唯一,全局访问),但它有很大问题:命名空间污染。应该避免使用

  • 使用命名空间,其实就是用一个对象把一类全局变量转为属性,这样可以很大程度减少全局变量的命名空间污染(全局变量本身也是window的属性,本质并无不同)。
  • 闭包封装私有变量,外部访问不到

2 策略模式

定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换(有相同的目标和意图)

情景:根据员工薪资和绩效来发年终奖

先看最容易想到的代码实现

const calculateBonus = (preformanceLevel, salary) {
  let bonus;
  switch(preformanceLevel) {
    case 'S' :
      bonus = salary * 4;
      break;
    case 'A' :
      bonus = salary * 3;
      break;
    case 'B' :
      bonus = salary * 2;
      break;
    default:
      break;
  }
  return bonus;
}
  • 算法复用性差,如果其他地方需要使用只能复制黏贴
  • 如果薪增一种特效等级,或者修改某个算法,必须修改calculateBonus函数的内部实现,违反了“开放-封闭原则” 用对象的多态性来改造一下
const strategies = {
  "S": function( salary ){
    return salary * 4;
  },
  "A": function( salary ){
    return salary * 3;
  },
  "B": function( salary ){
    return salary * 2; 
  }
};
const calculateBonus = (preformanceLevel, salary) {
  return strategies[preformanceLevel](salary);
}
  • 这样改造完calculateBonus函数就基本不需要修改了,需求变动只需要修改strategies对象,而这个封装了算法(业务规则)的对象也可以在其他地方复用

3 代理模式

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

书上的情景案例很想跟大家分享下~ 懂了吧!至于为什么要这么麻烦通过别人来送,也有后续解释,可以说合情合理了 代码就不放了~其实主要是理解什么是代理以及代理的好处,下面列几个工作中会遇到的代理场景:

  • 请求数据为解决跨域问题,使用前端代理
  • 防抖节流
  • 图片预加载(大图片加载出来前用占位图代替)
  • 前面说到的单例模式,需要点登录时出现登录框,又不希望重复创建dom,通过代理实现单例效果

3.1 保护代理和虚拟代理

还是用送花的场景,作者表述的很清楚而真实 图片预加载就是一种虚拟代理

3.2 代理和本体接口的一致性

从用户的角度,代理是为了实现额外的需求,可能因为需求变动,增加或放弃了额外需求,接口一致就可以令代理和本体的替换使用非常方便

总结

虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。

4 迭代器模式

指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示

简单的说就是数据结构能实现统一遍历接口(具有length属性且能通过下标访问),

4.1 内部迭代器

已经定义好了迭代规则,一次调用即可

数组的很多方法就是内部迭代器,常见的map、forEach、some、every,

4.2 外部迭代器

外部迭代器必须显示请求迭代下一个元素

外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工控制 迭代的过程或者顺序。

总结

内置的数组迭代方法已经可以解决大部分迭代需求;迭代不仅局限于数组,类数组对象如函数的arguments也可以进行迭代

5 发布-订阅模式

当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。也叫观察者模式

5.1 常见使用场景

  通过addEventListener监听dom元素的click事件就是一种发布-订阅模式:可以给dom元素的click事件设置多个监听函数,事件触发时所有监听函数都会执行。

  React项目中使用redux、dva、umi提供的useModel插件等等实现全局状态共享, 当状态发生改变时触发所有使用该状态的组件进行更新

5.2 如何实现

示例:售楼部给所有有意向购房的潜在用户发送开盘通知

  • 指定谁充当发布者(售楼部)
  • 给发布者添加一个缓存列表,用户存放回调函数以便通知订阅者(售楼处的花名册)
  • 发布消息时,发布者遍历缓存列表,依次触发里面存放的订阅者回调函数(遍历花名册,挨个发短信)