写点什么

前端需要掌握的设计模式

  • 2021-07-31
  • 本文字数:7101 字

    阅读完需:约 23 分钟

前端需要掌握的设计模式

提到设计模式,相信知道的同学都会脱口而出,五大基本原则(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-07-31 18:003789

评论

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

自助洗车方便主要体现在哪方面

共享电单车厂家

自助洗车 自助洗车加盟

在线文档协作:办公必备高效率神器

小炮

启动!阿里巴巴编程之夏2022

阿里巴巴云原生

阿里云 云原生 编程之夏

力扣每日一练之双指针1Day8

京与旧铺

6月月更

自助洗车到底有哪些不一样的地方

共享电单车厂家

自助洗车加盟 24小时自助洗车

自助洗车一次费用不到10元你敢信

共享电单车厂家

自助洗车加盟 自助洗车费用

Move Protocol Beta测试版进行时,瓜分生态核心权益MOMO

鳄鱼视界

Docker入坑篇

青柚1943

Docker DevOps 云原生 容器化

Dubbo3 官方文档贡献者征集令

阿里巴巴云原生

阿里云 开源 云原生 dubbo

技术实践 | 场景导向的音视频通话体验优化

融云 RongCloud

数据库主键一定要自增吗?有哪些场景不建议自增?

CRMEB

Gartner 网络研讨会 “九问数字化转型” 会后感

明哥的IT随笔

数字化转型

天天在都在谈的S3协议到底是什么?一文带你了解S3背后的故事

wljslmz

对象存储 S3 6月月更

你的城市有24小时共享自助洗车吗

共享电单车厂家

自助洗车加盟 自助洗车店

数字藏品系统开发,NFT艺术品交易平台搭建

薇電13242772558

NFT 数字藏品

Nebula Graph入驻阿里云计算巢,助力企业打造云上超大规模图数据库

阿里云弹性计算

spark 分布式 云原生 图数据库 计算巢

阿里云 ACK One、ACK 云原生 AI 套件新发布,解决算力时代下场景化需求

阿里巴巴云原生

阿里云 云原生 分布式云容器平台 ACK One ACK 云原生 AI 套件

有哪些好用的工作汇报工具

优秀

低代码 工具软件

带你区分几种并行

华为云开发者联盟

后端 开发 华为云

揭秘支撑百度搜索、Feed、小程序三大业务的MVVM框架设计思想,San 核心人员倾力打造

图灵教育

前端开发 好书推荐 框架设计

秒云云原生信创全兼容解决方案再升级,助力信创产业加速落地

MIAOYUN

运维 云原生 信创 智能运维 信创云

SQL操作:WITH表达式及其应用

华为云开发者联盟

数据库 sql 后端 华为云

拥抱开放,Serverless 时代的下一征程

阿里巴巴云原生

阿里云 Serverless 云原生 SAE

Move Protocol Beta测试版稳定,临时决定奖池规模再扩大

小哈区块

如何使用 DATAX 以 UPSERT 语义更新下游 ORACLE 数据库中的数据

明哥的IT随笔

oracle 大数据 数据仓库 DataX

深入剖析 HIVE 的锁和事务机制

明哥的IT随笔

大数据 hive 数据仓库

昇腾科研创新使能计划赋能开发者  华为计算提供三大维度支持

Geek_2d6073

Serverless 时代下微服务应用全托管解决方案

阿里巴巴云原生

阿里云 Serverless 微服务 云原生

pnpm 中无法使用 patch-package 打补丁

OpenHacker

前端 js

自助洗车为什么可以做到24小时营业

共享电单车厂家

自助洗车机 自助洗车加盟 24小时自助洗车

为 Serverless Devs 插上 Terraform 的翅膀,实现企业级多环境部署(上)

阿里巴巴云原生

阿里云 Serverless 云原生 开源项目

前端需要掌握的设计模式_语言 & 开发_微医大前端技术_InfoQ精选文章