写点什么

前端需要掌握的设计模式

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

评论

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

最全面的采样器:Native Instruments Maschine 3

Rose

微信伴侣WechatTweak for mac(微信防撤回、多开助手) 中文集成版

Rose

专业的字体设计软件 FontLab for Mac

Rose

安全与智能双加持,Coremail XT6解锁高效办公密码

科技热闻

Easysearch 移除 Master 节点注意事项

极限实验室

数据库·

哈尔滨等保测评数据隐私与合规风险

黑龙江陆陆信息测评部

BOE(京东方)发布2024年可持续发展报告 以多维创新路径构建绿色未来

科技热闻

MacBooster 8 mac版 苹果电脑一站式系统清理维护工具

Rose

阿维塔或将首批搭载ADS4

科技热闻

Tampermonkey for Mac(油猴Safari浏览器辅助插件)中文版

Rose

LightWave 3D 2020 for mac 附激活秘钥 3D动画制作

Rose

MCP协议重大升级,Spring AI Alibaba联合Higress发布业界首个Streamable HTTP实现方案

阿里巴巴云原生

阿里云 云原生

《Operating System Concepts》阅读笔记:p764-p766

codists

操作系统

长安马自达全球车型MAZDA 6e启航欧洲,全球化战略迈入新里程

科技热闻

拥抱国产化:转转APP的鸿蒙NEXT端开发尝鲜之旅

JackJiang

网络编程 即时通讯 IM

Amazon Q 从入门到精通 – 加速构建亚马逊云基础设施

亚马逊云科技 (Amazon Web Services)

中国游戏出海迎来新机遇,腾讯游戏安全ACE助力应对安全挑战

新消费日报

OCR技术难点解读:数学公式检测与识别

合合技术团队

人工智能 算法 OCR OCR识别 #大数据

DistilQwen2.5-DS3-0324发布:知识蒸馏+快思考=更高效解决推理难题

阿里云大数据AI技术

阿里云 LLM PAI 大模型蒸馏 DistilQwen2.5

如何高效智能地进行设备数据采集?

万界星空科技

数据采集 mes 万界星空科技mes 设备数据采集 设备管理软件

开源之夏2025 Apache DolphinScheduler课题宣讲

白鲸开源

大数据 开源 Apache DolphinScheduler 任务调度 开源之夏

个人开发者如何发送短信?这个方案太香了!

外滩运维专家

短信验证码 短信验证 短信发送 个人短信 短信群发

Photoshop 2024 (PS2024中文激活补丁)直装绿色版

Rose

参考文献管理工具 EndNote 21 大客户授权-mac/win

Rose

用友助力郴电国际司库建设项目成功上线,战略合作开启数智化转型新征程

用友智能财务

长安马自达全球车型MAZDA 6e启航欧洲,全球化战略迈入新里程

极客天地

DolphinScheduler开发者必看!IDEA本地调试实战指南

白鲸开源

开源 Apache DolphinScheduler 任务调度

虚拟云环境中的高性能块设备:直通方法比较

Sergey Platonov

Performance 虚拟化 高性能计算,

音乐创作 Ableton Live 12 Suite永久许可证-mac/win

Rose

Photomatix Pro for Mac(专业HDR图像处理软件)v7.1.16激活版

Rose

Understand 7 优秀的代码分析工具

Rose

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