写点什么

前端需要掌握的设计模式

  • 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:003975

评论

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

清明节特辑 |记忆存储、声音还原、性格模仿……AI可以让人类永生吗?

华为云开发者联盟

AI 语音合成 清明节 对话机器人 VR/AR

Rust从0到1-所有权-引用和借用

rust 引用 所有权 借用

业务随行:用户的网络访问策略还能这么玩

华为云开发者联盟

网络 通信 安全组 IP地址 业务随行

8x Flow 业务建模法(一):你能分清业务和领域吗?

胡皓

领域驱动设计 DDD 架构设计 事件风暴 业务建模

软件测试分类体系,系统学习

程序员阿沐

软件测试 测试工程师 黑盒测试 白盒测试 测试类型

在npm发布自己的组件

空城机

JavaScript 大前端 npm 4月日更 自定义组件

那些我磕过的音视频项目总结

梅芳姑

单片机异常复位后如何保存变量数据

不脱发的程序猿

嵌入式 单片机 4月日更 硬件研发 单片机异常复位

MySql数据库列表数据分页查询、全文检索API零代码实现

crudapi

全文检索 API crud crudapi 列表查询

用DeBug的方式,带你掌握HBase文件在Snapshot的各种变化

华为云开发者联盟

HBase 元数据 数据迁移 数据备份 Snapshot

融云推出超值套餐包,音视频20万分钟免费享

融云 RongCloud

Java开发8年,40W年薪被别人叫垃圾?请你们不要口嗨了,好好去刷题吧!

Java架构追梦

Java 架构 面试 金三银四 年薪40W

Netty HashedWheelTimer 时间轮源码详解

Yano

Java 架构 Netty

重磅官宣:Nacos2.0 发布,性能提升 10 倍

阿里巴巴云原生

Java 容器 微服务 云原生 应用服务中间件

如何美化 GitHub 个人主页?

彭宏豪95

GitHub 写作 markdown IT 4月日更

NAC公链主打应用而生的NA(Nirvana)公链有什么过人之处?

区块链第一资讯

短视频编辑:基于ExoPlayer可实时交互的播放器

梅芳姑

Hexo + Material + Github 搭建博客

U2647

博客 4月日更

Kubernetes 稳定性保障手册 -- 可观测性专题

阿里巴巴云原生

Serverless 容器 云原生 k8s 存储

OpenTelemetry 简析

阿里巴巴云原生

容器 开发者 云原生 k8s 监控

flink流计算可视化web平台

无情

sql 流计算 flin

将AI部署到现实?或许你该读读这本书!

澳鹏Appen

人工智能 大数据 AI 伦理

荷小鱼 x mPaaS | 借助 H5 容器改善 App 白屏、浏览器兼容等问题

蚂蚁集团移动开发平台 mPaaS

html5 mPaaS 离线包 教育科技

盘点几代会声会影图标

奈奈的杂社

Serverless 可观测性的过去、现在与未来

阿里巴巴云原生

Serverless 容器 开发者 云原生 调度

SCF—BSS3.0的“公路网”

鲸品堂

工具 框架搭建 流式计算框架

融云X-Meetup南京站 探讨实时通信架构的高质量设计

融云 RongCloud

货运物流移动端解决方案:为货运物流行业打造高性能、高粘性的“双端”触点

蚂蚁集团移动开发平台 mPaaS

移动开发 mPaaS 移动端 智慧物流

自己搭建一个语音聊天室

anyRTC开发者

ios android 音视频 WebRTC RTC

定义边缘计算架构需考虑的三个方面

边缘计算

二次元界福音:MakeGirlsMoe创建动漫人物

不脱发的程序猿

GitHub 开源 4月日更 二次元 MakeGirlsMoe

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