前言
谈到设计模式,有人觉得很难,学不会,有人觉得离前端很远,不需要学习,有人可能没听说过,谈不上学习,总有许多的原因,你和设计模式擦肩而过,确实,不需要设计模式,一样能完成相应功能,它不是前端的必须品,但我们经常遇到一些情况,比如继承了前同事的屎山级项目,所有逻辑混在一起,阅读起来很难受,想要扩展一个新功能,要把整个流程重新自测一遍,可维护性很差。另一方面,你觉得自己的代码不够健壮,你要想要实现更加优雅,扩展性更强的代码,但是没有理论支持,没有套路可用,有心无力,而设计模式就是为了解决这些问题。
前几天经历了一场面试,聊到了关于设计模式,事后做了些思考和总结,建了个专栏,准备写几篇关于前端比较常用的设计模式,以及在日常开发中的具体应用,欢迎点赞,收藏。
策略模式
定义
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换;
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换;
仅仅去看策略模式的定义,有些深奥难懂,不知所云,没有画面感,后续会通过多个日常开发中的示例让其生动形象且易于理解。它的作用除了解决设计模式的基础特性,高扩展和高复用性外,主要用于解决冗长的 if-else 或 switch 分支判断,拆分胖逻辑,让我们的代码有了更高的可读性和可维护性。
结论: 包含较多的if-else 或 switch
,并且代码的复杂性较高,需要较强的复用性和扩展性可以考虑使用策略模式来优化代码,但设计模式都是需要灵活使用的,比如判断只有一两个,代码的逻辑也很简单,没有什么复用关联性的,那就不需要多此一举,搞得代码更加繁琐,画蛇添足,降低代码的可维护性,那就得不偿失了。
年终奖计算示例
公司计算年终奖时,不同的部门有不同的计算方式,初定有以下三类:
- 类型A,月薪的0.8加上销售额的0.5
- 类型B,月薪的1.2加上销售额的0.3
- 类型C,月薪的2倍
这个需求一看就很简单,我用脚想都能实现,所以马不停蹄的实现了如下代码,顺手拿起旁边的肥宅快乐水,美滋滋的喝了一口,对于自己的开发效率赞不绝口,却不知这就是屎山级项目的源头。
// 计算年终奖
function getAnnualBonus(type, salary, sales) {
// 计算typeA年终奖
if (type === 'typeA') {
return salary * 0.8 + sales * 0.5
}
// 计算typeB年终奖
if (type === 'typeB') {
return salary * 1.2 + sales * 0.3
}
// 计算typeC年终奖
if (type === 'typeC') {
return salary * 2
}
}
上面这段代码的实现,看起来没什么问题,也能实现需求,但这是因为举例的计算方式比较简单,如果需要根据销售额梯度进行计算,那整个逻辑就会变得庞大起来,许多问题也更能体现。
对于getAnnualBonus
函数本身来讲,我们在一个函数里面实现三块比较重的逻辑,可读性和bug排错都是比较繁琐的,而对于不同类型的计算方式,也谈不上复用性,当我们在某个地方需要使用typeA
的算法时,除了ctrl c ctrl v
别无办法,但是你使用了复制黏贴,同一块逻辑在不同位置实现,会提高很大的维护成本和出错的可能性,当以后需要修改typeA
的计算逻辑时,你需要在多个位置修改,有可能出现改的不一致或者漏改的情况,提高了我们出错的比例,造成这个问题的原因是违背单一职责原则
。
另外一个问题是违背了开放封闭原则
,这个原则可能很多人都听过,但是没有太深的体会,假设我现在需要新增一种类型,计算方式为月薪的3倍加上销售额的0.2,之前的代码只能像以下这么改造,这样子的影响是修改了原函数,那就需要对于整个流程重新进行一次测试,避免由于其他原因改到其他计算方式出错,同时,越来越多的逻辑集成到一个函数下,造就了维护性更加低。
// 计算年终奖
function getAnnualBonus(type, salary, sales) {
// 计算typeA年终奖
if (type === 'typeA') {
return salary * 0.8 + sales * 0.5
}
// 计算typeB年终奖
if (type === 'typeB') {
return salary * 1.2 + sales * 0.3
}
// 计算typeC年终奖
if (type === 'typeC') {
return salary * 2
}
// 计算typeD年终奖
if (type === 'typeD') {
return salary * 3 + sales * 0.2
}
}
单一职责改造
这样子改造后,实现了每个函数功能独立,提高了代码的可读性和可复用性,由于函数的粒度变小了,bug的调试也会方便许多。
// 计算typeA年终奖
const handleTypeA = (salary, sales) => {
return salary * 0.8 + sales * 0.5
}
// 计算typeB年终奖
const handleTypeB = (salary, sales) => {
return salary * 1.2 + sales * 0.3
}
// 计算typeC年终奖
const handleTypeC = (salary, sales) => {
return salary * 2
}
// 计算年终奖
function getAnnualBonus(type, salary, sales) {
if (type === 'typeA') {
return handleTypeA(salary, sales)
}
if (type === 'typeB') {
return handleTypeB(salary, sales)
}
if (type === 'typeC') {
return handleTypeC(salary, sales)
}
}
开放封闭改造
上面虽然实现了每个函数的的功能独立,但是对于功能的扩展依旧束手无策,还是需要在源函数添加if xxx
,这样子并没有实现我们对于扩展开放,对于修改封闭的原则,所以还需要进一步思考,怎么才能实现新类型的扩展,但是不需要修改getAnnualBonus
函数,从getAnnualBonus
函数可以看出一些规律,一种类型对应一个函数,聪明的你一定能想到这不就是key-value
格式吗,那答案也呼之欲出,通过对象的key和函数做映射,就能实现我们想要的良好的扩展性,不信我们往下看。
声明一个对象,实现类型和对应计算方式函数的映射,getAnnualBonus
函数也简洁许多,只需要根据不同类型,读取对象的计算逻辑函数,并进行执行即可。
const typeMap = {
// 计算typeA年终奖
typeA(salary, sales) {
return salary * 0.8 + sales * 0.5
},
// 计算typeB年终奖
typeB(salary, sales) {
return salary * 1.2 + sales * 0.3
},
// 计算typeC年终奖
typeC(salary, sales) {
return salary * 2
}
}
// 计算年终奖
function getAnnualBonus(type, salary, sales) {
return typeMap[type](salary, sales)
}
这时我们新增一种类型typeD,计算方式为月薪的3倍加上销售额的0.2,我们只需要对于typeMap重新扩展一个类型即可,而不需要修改原函数getAnnualBonus
,这样子我们的年终奖计算逻辑不仅消除了大量的if-else
判断逻辑,还实现了很强的代码复用性,可扩展性,可维护性。
typeMap.typeD = (salary, sales) => {
return salary * 3 + sales * 0.2
}
经历了这个示例的实现及持续优化过程,相信此时的你对于策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换;
有了更深的体会,一系列算法指的是不同类型年终奖的计算函数,我们将其独立抽离,使之满足单一职责原则
,这就是对于算法的封装过程,然后通过对象的key-value
映射的巧妙方式,使得这些计算函数(算法)可以根据类型进行替换,同时也拥有很强的扩展性。
多端登录方式示例
上面这个示例算是单个函数的粒度,我们再举个关于文件粒度的示例。
最近开发了基于uni-app
的移动端框架,因为支持跨端,所以需要不同端的登录相关逻辑,初期先实现APP
和微信小程序
两种方式,包含校验是否已经登录的checkToken,用于登录的Login和退出登录的Logout,以及一些各端特有的逻辑,他们的函数处理逻辑都不一致,也不相关,谈不上复用,从目前来看if-else
是不错的选择,当我们进一步观察,会发现,同样是由类型做区分,不同的类型包含不一样的算法逻辑,同时需要较强的可扩展性和可维护性,这不是巧了么,和我们刚学的策略模式不谋而合,直接排上用场,具体的逻辑实现如下,不过忽略各个函数的具体实现。
目录结构
/permission/app/index.js
// 用于实例化对象
import { create } from "../permission.js";
// 核心代码
function guards(config) {
return {
mode: "app",
checkToken() {
// 验证token
},
getSession() {
// 获取session
},
login() {
// 登录
},
logout() {
// 退出登录
},
};
}
// 实例化app相关逻辑对象
export default create({
name: "app",
guards,
});
/permission/mp-weixin/index.js
// 用于实例化对象
import { create } from "../permission.js";
// 核心代码
function guards(config) {
return {
mode: "mp-weixin",
checkToken() {
// 验证token
},
getSession() {
// 获取session
},
login() {
// 登录
},
logout() {
// 退出登录
},
};
}
// 实例化微信小程序相关逻辑对象
export default create({
name: "mp-weixin",
guards,
});
/permission/permission.js
// 用于做参数校验,起到对象构造器的作用
class Permission {
constructor({ name, guards }) {
if (typeof name !== "string") {
throw new Error("Permission: name 必须为字符串,不能为空");
}
if (typeof guards !== "function") {
throw new Error(`Permission: guards 必须为函数,不能为空`);
}
this.guards = guards;
this.name = name;
}
}
// 用于对象实例化
const create = (options) => {
return new Permission(options);
};
export { create, Permission };
/permission/index.js
import { Permission } from "./permission.js";
import appLogin from "./app/index.js";
import mpWeixinLogin from "./mp-weixin/index.js";
const Core = {
permissions: {},
instance: undefined,
register(instance) {
if (instance instanceof Permission) {
this.permissions[instance.name] = instance;
} else {
throw new Error(`${instance} 非 Permission 实例`);
}
},
// 根据对应的模式,执行对应实例的guards方法,生成对应实例
guards(config) {
const { mode } = config;
// 获取对应模式实例
this.instance = this.permissions[mode];
if (this.instance) {
return this.instance.guards(config);
}
return {};
},
};
// 注册对应实例,这里只是起到注册的作用,并没有实际的执行
Core.register(appLogin);
Core.register(mpWeixinLogin);
export default Core;
/main.js
import Vue from "vue";
import Permission from "./permission/index.js";
// 公共配置
const config = {
mode: "app",
};
// 根据配置生成对应类型的实例,并挂载到全局上
Vue.prototype.$permission = Permission.guards(config);
扩展
未来需要再扩展h5相关的登录逻辑,也是比较简单的,创建一个h5文件夹,实现h5独立的登录逻辑
然后在/permission/index.js
文件引入h5的入口文件,在执行Core.register(h5Login)注册h5的登录逻辑
这样子算是实现各自登录逻辑的单一性和较好的可扩展性。
小结
正如开头所说,不需要设计模式一样可以很棒的完成需求,但有时候需要的不仅仅是完成需求,如果项目需求都是从最开始确定后就不再变化,那怎么实现代码都无关紧要,但项目都是需要持续变化和迭代的,而在这持续迭代的过程中,怎么保证代码具有良好的扩展性、复用性、可维护性就格外重要了,而设计模式就是保证我们健壮代码的格式或者套路吧。
相信经过一步一步的分析,你对于策略模式有个更深的理解,它也算是学习成本比较低的设计模式,但是带来的成效还是很客观的,有了理论基础,在开发的时候就有了更多的选择,更多的方式去实现健壮的代码。