QCon全球软件开发大会9折优惠倒计时,了解详情 了解详情
写点什么

前端需要掌握的设计模式

2021 年 7 月 31 日

前端需要掌握的设计模式

提到设计模式,相信知道的同学都会脱口而出,五大基本原则(SOLID)和 23 种设计模式。SOLID 所指的五大基本原则分别是:单一功能原则、开放封闭原则、里式替换原则、接口隔离原则和依赖反转原则。逐字逐句诠释这五大基本原则违背了写这篇文章的初衷,引用社区大佬的理解,SOLID 可以简单概括为六个字,即“高内聚,低耦合”:


  • 层模块不依赖底层模块,即为依赖反转原则。

  • 部修改关闭,外部扩展开放,即为开放封闭原则。

  • 合单一功能,即为单一功能原则。

  • 知识要求,对外接口简单,即为迪米特法则。

  • 合多个接口,不如独立拆分,即为接口隔离原则。

  • 成复用,子类继承可替换父类,即为里式替换原则。


23 种设计模式分为“创建型”、“行为型”和“结构型”。具体类型如下图:



设计模式说白了就是“封装变化”。比如“创建型”封装了创建对象的变化过程,“结构型”将对象之间组合的变化封装,“行为型”则是抽离对象的变化行为。接下来,本文将以“单一功能”和“开放封闭”这两大原则为主线,分别介绍“创建型”、“结构型”和“行为型”中最具代表性的几大设计模式。


创建型

工厂模式


工厂模式根据抽象程度可分为三种,分别为简单工厂、工厂方法和抽象工厂。其核心在于将创建对象的过程封装其他,然后通过同一个接口创建新的对象。 简单工厂模式又叫静态工厂方法,用来创建某一种产品对象的实例,用来创建单一对象。


// 简单工厂class Factory {  constructor (username, pwd, role) {   this.username = username;    this.pwd = pwd;    this.role = role;  }}
class CreateRoleFactory { static create (username, pwd, role) {   return new Factory(username, pwd, role);  }}
const admin = CreateRoleFactory.create('张三', '222', 'admin');
复制代码


在实际工作中,各用户角色所具备的能力是不同的,因此简单工厂是无法满足的,这时候就可以考虑使用工厂方法来代替。工厂方法的本意是将实际创建对象的工作推迟到子类中。


class User { constructor (name, menuAuth) {   if (new.target === User) throw new Error('User 不能被实例化');    this.name = name;    this.menuAuth = menuAuth;  }}
class UserFactory extends User { constructor (...props) { super(...props); } static create (role) { const roleCollection = new Map([ ['admin', () => new UserFactory('管理员', ['首页', '个人中心'])], ['user', () => new UserFactory('普通用户', ['首页'])] ]) return roleCollection.get(role)(); }}
const admin = UserFactory.create('admin');console.log(admin); // {name: "管理员", menuAuth: Array(2)}const user = UserFactory.create('user');console.log(user); // {name: "普通用户", menuAuth: Array(1)}
复制代码


随着业务形态的变化,一个用户可能在多个平台上同时存在,显然工厂方法也不再满足了,这时候就要用到抽象工厂。抽象工厂模式是对类的工厂抽象用来创建产品类簇,不负责创建某一类产品的实例。


class User {  constructor (hospital) {    if (new.target === User) throw new Error('抽象类不能实例化!');    this.hospital = hospital;  }}// 浙一class ZheYiUser extends User {  constructor(name, departmentsAuth) {    super('zheyi_hospital');    this.name = name;    this.departmentsAuth = departmentsAuth;  }}// 萧山医院class XiaoShanUser extends User {  constructor(name, departmentsAuth) {    super('xiaoshan_hospital');    this.name = name;    this.departmentsAuth = departmentsAuth;  }}
const getAbstractUserFactory = (hospital) => { switch (hospital) { case 'zheyi_hospital': return ZheYiUser; break; case 'xiaoshan_hospital': return XiaoShanUser; break; }}
const ZheYiUserClass = getAbstractUserFactory('zheyi_hospital');const XiaoShanUserClass = getAbstractUserFactory('xiaoshan_hospital');
const user1 = new ZheYiUserClass('王医生', ['外科', '骨科', '神经外科']);console.log(user1);const user2 = newXiaoShanUserClass('王医生', ['外科', '骨科']);console.log(user2);
复制代码


小结: 构造函数和创建对象分离,符合开放封闭原则。

使用场景: 比如根据权限生成不同用户。


单例模式


单例模式理解起来比较简单,就是保证一个类只能存在一个实例,并提供一个访问它的全局接口。单例模式又分懒汉式和饿汉式两种,其区别在于懒汉式在调用的时候创建实例,而饿汉式则是在初始化就创建好实例,具体实现如下:


// 懒汉式class Single { static getInstance () {   if (!Single.instance) {     Single.instance = new Single();    }    return Single.instance;  }}
const test1 = Single.getInstance();const test2 = Single.getInstance();
console.log(test1 === test2); // true
复制代码


// 饿汉式class Single { static instance = new Single();
static getInstance () { return Single.instance; }}
const test1 = Single.getInstance();const test2 = Single.getInstance();
console.log(test1 === test2); // true
复制代码

小结: 实例如果存在,直接返回已创建的,符合开放封闭原则。

使用场景: Redux、Vuex 等状态管理工具,还有我们常用的 window 对象、全局缓存等。

原型模式


对于前端来说,原型模式在常见不过了。当新创建的对象和已有对象存在较大共性时,可以通过对象的复制来达到创建新的对象,这就是原型模式。


// Object.create()实现原型模式const user = { name: 'zhangsan',  age: 18};let userOne = Object.create(user);console.log(userOne.__proto__); // {name: "zhangsan", age: 18}

// 原型链继承实现原型模式class User { constructor (name) { this.name = name; } getName () { return this.name; }}
class Admin extends User { constructor (name) { super(name); } setName (_name) { return this.name = _name; }}
const admin = new Admin('zhangsan');console.log(admin.getName());console.log(admin.setName('lisi'));
复制代码

小结: 原型模式最简单的实现方式---Object.create()。

使用场景: 新创建对象和已有对象无较大差别时,可以使用原型模式来减少创建新对象的成本。


结构型

装饰器模式


讲装饰器模式之前,先聊聊高阶函数。高阶函数就是一个函数就可以接收另一个函数作为参数。


const add = (x, y, f) => { return f(x) + f(y);}const num = add(2, -2, Math.abs);console.log(num); // 4
复制代码


函数 add 就是一个简单的高阶函数,而 add 相对于 Math.abs 来说相当于一个装饰器,因此这个例子也可以理解为一个简单的装饰器模式。在 react 中,高阶组件(HOC)也是装饰器模式的一种体现,通常用来不改变原来组件的情况下添加一些属性,达到组件复用的功能。


import React from 'react';
const BgHOC = WrappedComponent => class extends React.Component { render () {   return (     <div style={{ background: 'blue' }}>       <WrappedComponent />      </div>    );  }}
复制代码


小结: 装饰器模式将现有对象和装饰器进行分离,两者独立存在,符合开放封闭原则和单一职责模式。

使用场景: es7 装饰器、vue mixins、core-decorators 等。


适配器模式


适配器别名包装器,其作用是解决两个软件实体间的接口不兼容的问题。以 axios 源码为例:

function getDefaultAdapter() {  var adapter;  // 判断当前是否是 node 环境  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {    // 如果是 node 环境,调用 node 专属的 http 适配器    adapter = require('./adapters/http');  } else if (typeof XMLHttpRequest !== 'undefined') {    // 如果是浏览器环境,调用基于 xhr 的适配器    adapter = require('./adapters/xhr');  }  return adapter;}
// http adaptermodule.exports = function httpAdapter(config) {  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {    ...  }}// xhr adaptermodule.exports = function xhrAdapter(config) {  return new Promise(function dispatchXhrRequest(resolve, reject) {    ...  }}
复制代码


其目的就是保证 node 和浏览器环境的入参 config 一致,出参 Promise 都是同一个。

小结: 不改变原有接口的情况下,统一接口、统一入参、统一出参、统一规则,符合开发封闭原则。

使用场景 :拥抱变化,兼容代码。


代理模式

代理模式就是为对象提供一个代理,用来控制对这个对象的访问。在我们业务开发中最常见的有四种代理类型:事件代理,虚拟代理、缓存代理和保护代理。本文主要介绍虚拟代理和缓存代理两类。 提到虚拟代理,其最具代表性的例子就是图片预加载。预加载主要是为了避免网络延迟、或者图片太大引起页面长时间留白的问题。通常的解决方案是先给 img 标签展示一个占位图,然后创建一个 Image 实例,让这个实例的 src 指向真实的目标图片地址,当其真实图片加载完成之后,再将 DOM 上的 img 标签的 src 属性指向真实图片地址。

class ProxyImg { constructor (imgELe) {   this.imgELe = imgELe;    this.DEFAULT_URL = 'xxx';  }  setUrl (targetUrl) {   this.imgEle.src = this.DEFAULT_URL;    const image = new Image();        image.onload = () => {     this.imgEle.src = targetUrl;    }    image.src = targetUrl;  }}
复制代码


缓存代理常用于一些计算量较大的场景。当计算的值已经被出现过的时候,不需要进行第二次重复计算。以传参求和为例:

const countSum = (...arg) => { console.log('count...');  let result = 0;  arg.forEach(v => result += v);  return result;}
const proxyCountSum = (() => { const cache = {}; return (...arg) => { const args = arg.join(','); if (args in cache) return cache[args]; return cache[args] = countSum(...arg); };})()
proxyCountSum(1,2,3,4); // count... 10proxyCountSum(1,2,3,4); // 10
复制代码


小结: 通过修改代理类来增加功能,符合开放封闭模式。

使用场景: 图片预加载、缓存服务器、处理跨域以及拦截器等。


行为型

策略模式


介绍策略模式之前,简单实现一个常见的促销活动规则:


  • 预售活动,全场 9.5 折

  • 大促活动,全场 9 折

  • 返场优惠,全场 8.5 折

  • 限时优惠,全场 8 折


人人喊打的 if-else


const activity = (type, price) => { if (type === 'pre') {   return price * 0.95;  } else if (type === 'onSale') {   return price * 0.9;  } else if (type === 'back') {   return price * 0.85;  } else if (type === 'limit') {   return price * 0.8;  }}
复制代码


以上代码存在肉眼可见的问题:大量 if-else、可扩展性差、违背开放封闭原则等。 我们再使用策略模式优化:


const activity = new Map([ ['pre', (price) => price * 0.95],  ['onSale', (price) => price * 0.9],  ['back', (price) => price * 0.85],  ['limit', (price) => price * 0.8]]);
const getActivityPrice = (type, price) => activity.get(type)(price);
// 新增新手活动activity.set('newcomer', (price) => price * 0.7);
复制代码


小结: 定义一系列算法,将其一一封装起来,并且使它们可相互替换。符合开放封闭原则。

使用场景: 表单验证、存在大量 if-else 场景、各种重构等。

观察者模式


观察者模式又叫发布-订阅模式,其用来定义对象之间的一对多依赖关系,以便当一个对象更改状态时,将通知其所有依赖关系。通过“别名”可以知道,观察者模式具备两个角色,即“发布者”和“订阅者”。正如我们工作中的产品经理就是一个“发布者”,而前后端、测试可以理解为“订阅者”。以产品经理建需求沟通群为例:


// 定义发布者类class Publisher {  constructor () {    this.observers = [];    this.prdState = null;  }  // 增加订阅者  add (observer) {    this.observers.push(observer);  }  // 通知所有订阅者  notify () {    this.observers.forEach((observer) => {      observer.update(this);    })  }  // 该方法用于获取当前的 prdState  getState () {    return this.prdState;  }
// 该方法用于改变 prdState 的值 setState (state) { // prd 的值发生改变 this.prdState = state; // 需求文档变更,立刻通知所有开发者 this.notify(); }}
// 定义订阅者类class Observer { constructor () { this.prdState = {}; } update (publisher) { // 更新需求文档 this.prdState = publisher.getState(); // 调用工作函数 this.work(); } // work 方法,一个专门搬砖的方法 work () { // 获取需求文档 const prd = this.prdState; console.log(prd); }}
// 创建订阅者:前端开发小王const wang = new Observer();// 创建订阅者:后端开发小张const zhang = new Observer();// 创建发布者:产品经理小曾const zeng = new Publisher();// 需求文档const prd = { url: 'xxxxxxx'};// 小曾开始拉人入群zeng.add(wang);zeng.add(zhang);// 小曾发布需求文档并通知所有人zeng.setState(prd);
复制代码


经常使用 Event Bus(Vue) 和 Event Emitter(node)会发现,发布-订阅模式和观察者模式还是存在着细微差别,即所有事件的发布/订阅都不能由发布者和订阅者“私下联系”,需要委托事件中心处理。以 Vue Event Bus 为例:


import Vue from 'vue';
const EventBus = new Vue();Vue.prototype.$bus = EventBus;
// 订阅事件this.$bus.$on('testEvent', func);// 发布/触发事件this.$bus.$emit('testEvent', params);
复制代码


整个过程都是 this.$bus 这个“事件中心”在处理。

小结: 为解耦而生,为事件而生,符合开放封闭原则。

使用场景: 跨层级通信、事件绑定等。


迭代器模式


迭代器模式号称“遍历专家”,它提供一种方法顺序访问一个聚合对象中的各个元素,且不暴露该对象的内部表示。迭代器又分内部迭代器(jquery.each/for...of)和外部迭代器(es6 yield)。 在 es6 之前,直接通过 forEach 遍历 DOM NodeList 和函数的 arguments 对象,都会直接报错,其原因都是因为他们都是类数组对象。对此 jquery 很好的兼容了这一点。 在 es6 中,它约定只要数据类型具备 Symbol.iterator 属性,就可以被 for...of 循环和迭代器的 next 方法遍历。


(function (a, b, c) { const arg = arguments;  const iterator = arg[Symbol.iterator]();    console.log(iterator.next()); // {value: 1, done: false}  console.log(iterator.next()); // {value: 2, done: false}  console.log(iterator.next()); // {value: 3, done: false}  console.log(iterator.next()); // {value: undefined, done: true}})(1, 2, 3)
复制代码


通过 es6 内置生成器 Generator 实现迭代器并没什么难度,这里重点通 es5 实现迭代器:


function iteratorGenerator (list) {  var index = 0;  // len 记录传入集合的长度  var len = list.length;  return {    // 自定义 next 方法    next: funciton () {      // 如果索引还没有超出集合长度,done 为 false      var done = index >= len;      // 如果 done 为 false,则可以继续取值      var value = !done ? list[index++] : undefined;
// 将当前值与遍历是否完毕(done)返回 return { done: done, value: value }; } }}
var iterator = iteratorGenerator([1, 2, 3]);console.log(iterator.next()); // {value: 1, done: false}console.log(iterator.next()); // {value: 2, done: false}console.log(iterator.next()); // {value: 3, done: false}console.log(iterator.next()); // {value: undefined, done: true}
复制代码


小结: 实现统一遍历接口,符合单一功能和开放封闭原则。

使用场景: 有遍历的地方就有迭代器。

写到最后


设计模式的难,在于它的抽象和分散。抽象在于每一设计模式看例子都很好理解,真正使用起来却不知所措;分散则是出现一个场景发现好几种设计模式都能实现。而解决抽象的最好办法就是动手实践,在业务开发中探索使用它们的可能性。本文大致介绍了前端领域常见的 9 种设计模式,相信大家在理解的同时也不难发现,设计模式始终围绕着“封装变化”来提供代码的可读性、扩展性、易维护性。所以当我们工作生活中,始终保持“封装变化”的思想的时候,就已经开始体会到设计模式精髓了。



头图:Unsplash

作者:王君

原文:https://mp.weixin.qq.com/s/0W7yAU9sDkdn-zsaZ9Lv0Q

原文:前端需要掌握的设计模式

来源:微医大前端技术 - 微信公众号 [ID:wed_fed]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021 年 7 月 31 日 18:00936

评论

发布
暂无评论
发现更多内容

系统性能的主要技术指标以及变化

皮蛋

【Java基础】-- instanceof 用法详解

秦怀杂货店

Java

JDBC【4】-- jdbc预编译与拼接sql对比

秦怀杂货店

sql JDBC

第七周作业

孤星

学习总结--week11

张荣召

架构师训练营 1 期第 11 周:安全稳定 - 作业

piercebn

极客大学架构师训练营

架构师训练营第 1 期第 11 周作业

好吃不贵

极客大学架构师训练营

使用PicGo存储markdown图片(阿里云或者github)

秦怀杂货店

markdown 图床

到手的股权,又没了 | 法庭上的CTO(2)

赵新龙

股权 CTO 28天写作

11.3安全架构:反垃圾与风控

张荣召

【java基础】-- java接口和抽象类的异同分析

秦怀杂货店

Java 接口

从华为看VUCA时代如何让组织不断乘风破浪?

Alan

华为 战略思考 组织发展 组织活力

java集合【10】——— LinkedList源码解析

秦怀杂货店

Java 集合 linkedlist

Mybatis【6】-- Mybatis插入数据后自增id怎么获取?

秦怀杂货店

mybatis

七、性能

Geek_28b526

架构师训练营第11周总结

吴传禹

极客大学架构师训练营

JVM,JRE,JDK之间的区别和联系

入门小站

JVM

11.5高可用:提升系统可用性的架构方案

张荣召

架构词典:缓存

lidaobing

缓存 架构

11.4高可用:可用性度量

张荣召

11.6高可用:架构运维方案

张荣召

11.7高可用故障案例分析

张荣召

架构师训练营第 1 期第 11 周学习总结

好吃不贵

极客大学架构师训练营

架构师第十一周作业

_

极客大学架构师训练营 第十一周作业

第七周总结

孤星

11.8作业

张荣召

11.1安全架构:Web攻击与防护

张荣召

11.2安全架构:加密与解密

张荣召

架构师训练营第 1 期 -- 第十一周作业

发酵的死神

极客大学架构师训练营

IT做得好的时候,是什么状态?

boshi

职业

Mybatis【5】-- Mybatis多种增删改查那些你会了么?

秦怀杂货店

Java mybatis JDBC

数据库运维技术发展与展望

数据库运维技术发展与展望

前端需要掌握的设计模式-InfoQ