Blogs
Blogs copied to clipboard
浅谈前端设计模式
不知道有没有跟我一样,对于设计模式存在困惑的,以我为例,不瞒大家,我对设计模式真的是“看山就是山”、“看水就是水”、“看理论就是看理论”。毕业之前,我自己捣鼓的东西/项目,几乎没有设计的思想。老夫才不管那么多,直接梭哈,But,正是因为这样,才导致因设计缺陷、代码实现缺陷,为后期维护、开发迭代,带来了麻烦。
很真实,职级晋升有一点要求就是需要掌握设计模式,开玩笑,我是那种为了金钱
低头的人吗?我是真的想学习,所以,工作之余,也有去看《JavaScript 设计模式》,但真的就只掌握了一些理论知识,未实战过。
不是我不想实战,而是我没有形成这种潜意识,没有在日常开发中,能想到原来这块逻辑、这个核心模块可以这么设计~
这不,前两天,组内开了一次组里内部分享,分享主题为:《前端设计模式》,听完了这场分享,嗯,一语惊醒梦中人,这不还热乎着,赶紧记录一下
- 主要讲
- 策略模式
- 发布-订阅模式
- 装饰器模式
- 适配器模式
- 代理模式
- 责任链模式
正文开始
虽然前边我说不讲干巴巴的理论知识,但是呢,我们还是走一下流程的。就跟你相亲一样,你总不能一上来直接说我有房有车,无不良嗜好...最起码简单介绍一下吧~
什么是设计模式?
官方解释一点就是 : 模式是一种可服用的解决方案,用于解决软件设计中遇到的常见问题。
说白了,就是套路,举个 🌰,我们玩贪玩蓝月
,你第一关用了半小时,第二关用了一小时,第三关用了两小时....
好了,你最强,你花了一个月练到了满级,于是你开始练第二个号,这时候呢,其实你已经知道,每一关的捷径、好的装备在哪里,所以你按照这个套路
,很快的,20 天又练满了一个号。
身边有好友问你怎么这么快的又练了一个号,于是你为了造福大众,你写了一本 闯关攻略
~
我举的这个例子,你应该知道,什么是设计模式了吧?引用修言老哥的一句话 : 烹饪有菜谱,游戏有攻略,干啥都有一些能够让我们达到目标的“套路”,在程序世界,编程的“套路”就是设计模式。
当然,如果真要给你个定义 : 我认为,设计模式就是在软件设计、开发过程中,针对特定问题、场景的更优解决方案。
为什么会有设计模式?
就还是上边的 🌰,鲁迅先生说过 : “希望是本无所谓有,无所谓无的。这正如地上的路;其实地上本没有路,走的人多了,也便成了路”,设计模式是前辈们针对开发中遇到的问题,提出大家公认且有效的解决方案。
为什么需要设计模式?
可能有小伙伴确实没有用过,或者说用了但不知道这就是设计模式。那么为什么我们需要呢?是因为在我们遇到相似的问题、场景时,能快速找到更优的方式解决
如何使用?
在 JS 设计模式中,最核心的思想——封装变化,怎么理解,比如我们写一个东西,这个东西在初始 v1.0
的时候是这 B 样,到了 v5.0
、v10.0
甚至 v99.0
、v100.0
还是这 B 样,那 ojbk,你爱怎么写就怎么写,你只要实现就可以了。
设计模式的核心操作是去观察你整个逻辑里面的变与不变,然后将变与不变分离,达到使变化的部分灵活、不变的地方稳定的目的。
设计模式分类
都有啥设计模式 ? 相信了解的,都知道有 20 多种...
挺多的,我不扯那么多,其它的我没用过。什么享元模式、外观模式、生成器模式啥的,我用都没用过,我咋跟你们聊啊,我今天就唠嗑唠嗑几个前端常用的。
题目一
我们先来做一个题,很简单的,大家肯定都做过 权限逻辑
判断吧?
需求 : 只用当用户满足以下条件,才能看阿宽的这篇文章
给大家 3min
,代码怎么写? “ 呵,你这不是看不起老夫吗?老夫拿起键盘,就是 if-else
梭哈,直接带走,下一个 ! ”
function checkAuth(data) {
if (data.role !== 'juejin') {
console.log('不是掘金用户');
return false;
}
if (data.grade < 1) {
console.log('掘金等级小于 1 级');
return false;
}
if (data.job !== 'FE') {
console.log('不是前端开发');
return false;
}
if (data.type !== 'eat melons') {
console.log('不是吃瓜群众');
return false;
}
}
相信这段代码,大家都会写,那么这么写,有什么问题 ?
- checkAuth 函数会爆炸 💥
- 策略项无法复用
- 违反开闭原则(不知道开放封闭原则的自行百度)
聪明的小伙伴已经知道这里要讲的是什么模式了,对头!这里讲的就是 策略模式
。那么什么是策略模式呢 ?
策略模式
定义 : 要实现某一个功能,有多种方案可以选择。我们定义策略,把它们一个个封装起来,并且使它们可以相互转换。
策略 + 组合,绝配啊,老哥!
我们用策略模式来改造以下这段逻辑 👇
// 维护权限列表
const jobList = ['FE', 'BE'];
// 策略
var strategies = {
checkRole: function(value) {
return value === 'juejin';
},
checkGrade: function(value) {
return value >= 1;
},
checkJob: function(value) {
return jobList.indexOf(value) > 1;
},
checkEatType: function(value) {
return value === 'eat melons';
}
};
我们已经写完了策略,接下来要做的就是验证了~
// 校验规则
var Validator = function() {
this.cache = [];
// 添加策略事件
this.add = function(value, method) {
this.cache.push(function() {
return strategies[method](value);
});
};
// 检查
this.check = function() {
for (let i = 0; i < this.cache.length; i++) {
let valiFn = this.cache[i];
var data = valiFn(); // 开始检查
if (!data) {
return false;
}
}
return true;
};
};
此时,小彭同学需要进行权限验证的条件为 :
- 掘金用户
- 掘金等级 1 级以上
那么代码就可以这么写 :
// 小彭使用策略模式进行操作
var compose1 = function() {
var validator = new Validator();
const data1 = {
role: 'juejin',
grade: 3
};
validator.add(data1.role, 'checkRole');
validator.add(data1.grade, 'checkGrade');
const result = validator.check();
return result;
};
然后另一个小伙伴阿宽,他可能需要进行权限验证的条件为 :
- 掘金用户
- 前端工程师
那么代码就可以这么写 :
// 阿宽使用策略模式进行操作
var compose2 = function() {
var validator = new Validator();
const data2 = {
role: 'juejin',
job: 'FE'
};
validator.add(data2.role, 'checkRole');
validator.add(data2.job, 'checkJob');
const result = validator.check();
return result;
};
这是不是比一直疯狂写 if-else
好太多了呢?还有什么例子?表单验证啊 ~ 对于表单字段(名称、密码、邮箱、....)我们可以使用策略模式去设计优化它,想啥呢,赶紧动手试一下!我都已经手把手教你到这了~
什么时候用策略模式?
当你负责的模块,基本满足以下情况时
- 各判断条件下的策略相互独立且可复用
- 策略内部逻辑相对复杂
- 策略需要灵活组合
题目二
前面还逼逼一下,这里直接给需求了 👇
需求 : 申请成功后,需要触发对应的订单、消息、审核模块对应逻辑
机智如我,我会如何做呢?
function applySuccess() {
// 通知消息中心获取最新内容
MessageCenter.fetch();
// 更新订单信息
Order.update();
// 通知相关方审核
Checker.alert();
}
不就这样写吗,还想咋滴!!!是的,这么写没得毛病,但是呢,我们来思考几个问题
比如 MessageCenter.fetch()
是小彭写的,他大姨夫来了,心情不爽,把模块的方法名改了,现在叫 MessageCenter.request()
,你咋办,你这块逻辑改呗~
再比如,你和阿宽并行开发的,阿宽负责订单模块,你一气呵成写下这段代码,然后一运行,报错了,一询问,发现,原来阿宽昨晚去蹦迪了,原本今天应该完成的订单模块Order.update()
,延迟一天,那你就只能先注释代码,等依赖的模块开发完了,你再回来添加这段逻辑咯~
更可怕的是,你可能不只是涉及到这三个模块,maybe 还有很多模块,比如你申请成功,现在还需要上报申请日志,你总不能这样写吧?
function applySuccess() {
// 通知消息中心获取最新内容
MessageCenter.fetch();
// 更新订单信息
Order.update();
// 通知相关方审核
Checker.alert();
// maybe 更多
Log.write();
...
}
到这里,我们的 发布-订阅模式
要按捺不住了。
发布-订阅模式
啊哈哈哈,有没有觉得这个EventEmitter
好熟悉啊,这不是面试常会问的?
发布-订阅是一种消息范式,消息的发布者,不会将消息直接发送给特定的订阅者
,而是通过消息通道广播出去,然后呢,订阅者通过订阅获取到想要的消息。
我们用 发布-订阅模式 修改以下上边的代码 👇
const EventEmit = function() {
this.events = {};
this.on = function(name, cb) {
if (this.events[name]) {
this.events[name].push(cb);
} else {
this.events[name] = [cb];
}
};
this.trigger = function(name, ...arg) {
if (this.events[name]) {
this.events[name].forEach(eventListener => {
eventListener(...arg);
});
}
};
};
上边我们写好了一个 EventEmit
,然后我们的业务代码可以改成这样 ~
let event = new EventEmit();
event.trigger('success');
MessageCenter.fetch() {
event.on('success', () => {
console.log('更新消息中心');
});
}
Order.update() {
event.on('success', () => {
console.log('更新订单信息');
});
}
Checker.alert() {
event.on('success', () => {
console.log('通知管理员');
});
}
但是这样就没问题了吗?其实还是有弊端的,比如说,过多的使用发布订阅,就会导致难以维护调用关系。所以,还是看大家的设计吧,这里只是让大家知道,发布订阅模式是个啥~
什么时候用发布-订阅模式?
当你负责的模块,基本满足以下情况时
- 各模块相互独立
- 存在一对多的依赖关系
- 依赖模块不稳定、依赖关系不稳定
- 各模块由不同的人员、团队开发
我知道你有疑问,关于 观察者模式 VS 发布-订阅模式,这里我不讲它们的区分,下期再聊,或者自行资料查询
题目三
这个题目,也有点难想啊,我直接说吧,主要讲 装饰器模式、适配器模式。
装饰器模式
个人理解 : 是为了给一个函数赋能,增强它的某种能力,它能动态的添加对象的行为,也就是我传入的就是一个对象
在 JS 世界中,世间万物,皆为对象
大家过年,都会买桔子树吧(不买的统一带走),意味“大吉大利”嘛,那么我们买了桔子树之后,都会往上边挂一些红包,摇身一变,“红包桔子树”,牛掰!这个的红包就是装饰器,它不对桔子树原有的功能产生影响。
再举个 🌰,我现在写的这边文章,我只会写中文,但是各位看官中有英国小伙伴,那我不会写英文啊,所以我需要通过装饰器来赋予我写英文的能力
你这不是在真实写代码中的啊,能不能举一个日常开发的 🌰,ok,那我来举一个,React 中的高阶组件 HOC
了解 React 的都知道,高阶组件其实就是一个函数,接收一个组件作为参数,然后返回一个新的组件。
那么我们现在写一个高阶组件 HOC,用它来装饰 Target Component
import React from 'react';
const yellowHOC = WrapperComponent => {
return class extends React.Component {
render() {
<div style={{ backgroundColor: 'yellow' }}>
<WrapperComponent {...this.props} />
</div>;
}
};
};
export default yellowHOC;
定义了一个带有装饰黄色背景的高阶组件,我们用它来装饰目标组件
import React from 'react';
import yellowHOC from './yellowHOC';
class TargetComponent extends Reac.Compoment {
render() {
return <div>66666</div>;
}
}
export default yellowHOC(TargetComponent);
你看,我们这不就用到了装饰器模式了嘛?什么,你还听不懂?那我最后再举一个例子,不知道这个例子,能不能帮助你们理解
const kuanWrite = function() {
this.writeChinese = function() {
console.log('我只会写中文');
};
};
// 通过装饰器给阿宽加上写英文的能力
const Decorator = function(old) {
this.oldWrite = old.writeChinese;
this.writeEnglish = function() {
console.log('给阿宽赋予写英文的能力');
};
this.newWrite = function() {
this.oldWrite();
this.writeEnglish();
};
};
const oldKuanWrite = new kuanWrite();
const decorator = new Decorator(oldKuanWrite);
decorator.newWrite();
适配器模式
个人理解,为了解决我们不兼容的问题,把一个类的接口换成我们想要的接口。
举个 🌰 , 我想听歌的时候,我发现我没带耳机,我的手机是 iphone 的,而现在我只有一个 Type-C 的耳机,为了能够听歌,我用了一个转换器(也就是适配器),然后我就可以开心的听歌了。
我举个真实业务中的例子,前段时间用 umi/hooks 库中的 useBoolean API,涉及的文件有七八个,有一天,因为一些原因,我们决定暂时放弃此库,也就意味着,useBoolean
用不了了,你怎么办呢?
很简单嘛,找到这七八个文件,把 useBoolean 这段逻辑自己写不就好了吗?
👏 yes ! 这确实是一个解决方案,但问题来了,不麻烦吗?一段 useBoolean
逻辑代码,你要写七八遍?没错,这里我们的适配器模式就很有必要了。
假设原先我们的代码: (使用了 umi/hools 的useBoolean API)
import { useBoolean } from 'umi/hooks';
function Test() {
const visible = useBoolean(false); // 初始化fasle
...
visible.state; // 状态值
visible.isTrue(); // 设置为true
visible.isFalse(); // 设置为false
}
如果按照我们第一种思路,我们需要将每一个文件下的代码改成这样 👇
import React, { useState } from 'react';
function Test() {
const [visible, setVisible] = useState(false);
// 巴拉巴拉处理之后
setVisible(false); // 设置visible值为false
setVisible(true); // 设置visible值为true
}
你需要将七八个文件都改成这样,几十岁的程序员了,写这种代码你不觉得恶心吗?不要吵了不要吵了,先上模式先上模式 ~ (857, 857, 857 ...)
我们的问题是 : useBoolean API 不用了,导致 visible.state、visible.isTrue() 等字段不可用,从而导致需要自己写state去维护,逻辑都一样,那么我们是不是可以将其抽离出来?
export function useBooleanHooks(boolean = false) {
const [boolValue, setBoolValue] = useState(boolean);
const setTrue = () => setBoolValue(true);
const setFalse = () => setBoolValue(false);
return {
state: boolValue,
setTrue,
setFalse
};
}
我们实现了一个简易版的 useBooleanHook,但是你会发现,接口不兼容啊,怎么办,适配器用起来,再考一下大家,适配器是什么,干什么用?
为了解决我们不兼容的问题,把一个类的接口换成我们想要的接口
export function useBooleanHooks(boolean = false) {
//...
return {
state: boolValue,
isTrue: setTrue, // 将此类的接口(setTrue)换成我们想要的接口(isTrue)
isFalse: setFalse // 将此类的接口(setFalse)换成我们想要的接口(isFalse)
};
}
可能有人觉得,这就是适配器?你在逗我?我想说 : _________
题目四
我们再来讲一个叫做 代理模式,说到代理哈,我脑海里第一个浮现的词语 : “事件委托、事件代理”,这算吗?算哒。我举些 🌰,让大家知道代理模式是个啥玩意
作为程序员嘛,女朋友比较难找,就算找到了,咱这么瘦弱,怕是保护不了啊,所以我花钱找了个保镖来保护我,稳妥。这就是代理模式。
你翻qiang吗?你能 google 吗?老实人哪会什么翻qiang,我是不会的,会我也说我不会。其实正常来讲,我们直接访问 google 是无响应的。那怎么办呢,通过第三方代理服务器。小飞机?懂 ?
要说初中非主流三大巨头,莫过于 许嵩、徐良、汪苏泷
了,去年想去看许嵩演唱会,好家伙,这个演唱会的门票都被抢光了,无奈之下,只能找黄牛,这里,黄牛就起了代理的作用,懂?
程序世界的代理者也是如此,我们不直接操作原有对象,而是委托代理者去进行。代理者的作用,就是对我们的请求预先进行处理或转接给实际对象。
代理模式是为其它对象提供一种代理以控制这个对象的访问,具体执行的功能还是这个对象本身,就比如说,我们发邮件,通过代理模式,那么代理者可以控制,决定发还是不发,但具体发的执行功能,是外部对象所决定,而不是代理者决定。
// 发邮件,不是qq邮箱的拦截
const emailList = ['qq.com', '163.com', 'gmail.com'];
// 代理
const ProxyEmail = function(email) {
if (emailList.includes(email)) {
// 屏蔽处理
} else {
// 转发,进行发邮件
SendEmail.call(this, email);
}
};
const SendEmail = function(email) {
// 发送邮件
};
// 外部调用代理
ProxyEmail('cvte.com');
ProxyEmail('ojbk.com');
下边再来举一个例子,来至 《JavaScript 设计模式与开发实践》
// 本体
var domImage = (function() {
var imgEle = document.createElement('img');
document.body.appendChild(imgEle);
return {
setSrc: function(src) {
imgEle.src = src;
}
};
})();
// 代理
var proxyImage = (function() {
var img = new Image();
img.onload = function() {
domImage.setSrc(this.src); // 图片加载完设置真实图片src
};
return {
setSrc: function(src) {
domImage.setSrc('./loading.gif'); // 预先设置图片src为loading图
img.src = src;
}
};
})();
// 外部调用
proxyImage.setSrc('./product.png');
什么时候用代理模式?
当你负责的模块,基本满足以下情况时
- 模块职责单一且可复用
- 两个模块间的交互需要一定限制关系
这里我又知道你有疑问了,关于 代理模式 VS 装饰者模式,这里我不讲它们的区分,下期再聊,或者自行资料查询
题目五
需求 :如图所示,我们申请设备之后,接下来要选择收货地址,然后选择责任人,而且必须是上一个成功,才能执行下一个~
小伙伴们惊讶了,这不简单嘛?奥力给!
function applyDevice(data) {
// 处理巴拉巴拉...
let devices = {};
let nextData = Object.assign({}, data, devices);
// 执行选择收货地址
selectAddress(nextData);
}
function selectAddress(data) {
// 处理巴拉巴拉...
let address = {};
let nextData = Object.assign({}, data, address);
// 执行选择责任人
selectChecker(nextData);
}
function selectChecker(data) {
// 处理巴拉巴拉...
let checker = {};
let nextData = Object.assign({}, data, checker);
// 还有更多
}
你看,这不就完事了,有啥难的,然后过了第二天,你又接了两个新的流程需求,可能一个就两步骤,一个可能多了“检查库存”这个步骤
你不由惊了,哎呀妈呀,老夫聊发少年狂,键盘伺候,Ctrl C + Ctrl V,直接copy然后改一下逻辑??
这里就是要讲的责任链模式。
责任链模式
什么是责任链模式呢?我给你们找了个定义 : 避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
const Chain = function(fn) {
this.fn = fn;
this.setNext = function() {}
this.run = function() {}
}
const applyDevice = function() {}
const chainApplyDevice = new Chain(applyDevice);
const selectAddress = function() {}
const chainSelectAddress = new Chain(selectAddress);
const selectChecker = function() {}
const chainSelectChecker = new Chain(selectChecker);
// 运用责任链模式实现上边功能
chainApplyDevice.setNext(chainSelectAddress).setNext(chainSelectChecker);
chainApplyDevice.run();
这样的好处是啥?首先是解耦了各节点关系,之前的方式是 A 里边要写 B,B 里边写 C,但是这里不同了,你可以在 B 里边啥都不写。
其次,各节点灵活拆分重组,正如上边你接的两个新需求。比如两个步骤的你就只需要这么写完事
const applyLincense = function() {}
const chainApplyLincense = new Chain(applyLincense);
const selectChecker = function() {}
const chainSelectChecker = new Chain(selectChecker);
// 运用责任链模式实现上边功能
chainApplyLincense.setNext(chainSelectChecker);
chainApplyLincense.run();
什么时候使用责任链模式?
当你负责的模块,基本满足以下情况时
- 你负责的是一个完整流程,或你只负责流程中的某个环节
- 各环节可复用
- 各环节有一定的执行顺序
- 各环节可重组
结尾
不知不觉,又是给大家撸了一篇设计模式的文章,设计模式真的很重要,虽然说我也刚领悟其中的一丝诀窍,但是我想,可能还有很多跟我一样,在设计模式门槛外,迟迟无法顿悟的小伙伴,我是一个很烦看长篇大论的人,之前也看了设计模式相关书籍,初次看还能耐心看下去,但是去看别人博客、文章的时候,看到好多定义、好多理论,包括一些举的🌰,感觉当时懂了,但是在开发中,我还是不知道怎么用...
不是让大家强行套用设计模式,而是想表达 : 我们首先需要理解,其次需要形成一种肌肉记忆,正如前边说的策略模式、发布-订阅模式的例子一样,大家在真实开发场景中肯定都有遇到,只是没有想到,原来这就是设计模式,或者说,原来这里可以用到设计模式去设计。