从条件判断到策略模式
复杂判断的优雅写法
在实际的业务场景开发中,常用的判断有
if-else 流,同时兼有 switch-case , 三目运算符。
1、 if-esle
首先看看基本的逻辑处理。
/* 复杂度中规中矩,语义化差,且不常注释 */const GradeType = (a) => {if (a == 1) {console.log('A')} else if (a == 2) {console.log('B')} else if (a == 3) {console.log('B')} else if (a == 4) {console.log('C')}/* and more */}
问题:
1、条件判断多,易遗漏,后期不好修改2、要依次判断每个分支条件
2、借助 switch
const GradeTypeCase = (a) => {switch (a) {case 1:console.log("A");break;case 2:case 3:console.log("B");break;case 4:console.log("C");break;default:break;}};
switch 的问题在于,灵活度不够,针对值为 常量 可使用
3、三元表达式
对较简单的使用三元表达式
// 三元表达式ExamPass = price >= 60 ? "pass" : "fail";// if else 判断if (price >= 60) {return "pass";} else {return "fail";}
// 不滥用三目运算符if (!aup || !bup) {return a === doc? -1: b === doc? 1: aup? -1: bup? 1: sortInput? indexOf.call(sortInput, a) - indexOf.call(sortInput, b): 0;}
可选链 ?.
let user = {}; // user 没有 address 属性alert( user.address && user.address.street && user.address.street.name );// undefined(不报错)// 使用可选链alert( user?.address?.street ); // undefined(不报错)
4、表驱动 (配数据置和业务逻辑)
假设芝麻信用的场景
function showGrace(grace) {let level='';if(grace=700){level='信用极好'}// other codeelse if(grace=550){level='信用中等'}else{level='信用较差'}return level;}
⚡️ 需求变更了
- grace 有变
- 展示文字变更
暴露问题, 不够灵活,需要一个个条件去修改
来做个小修改
// 配置项let graceForLevel = [700, 650, 600, 550];let levelText = ["信用极好", "信用优秀", "信用良好", "信用中等", "信用较差"];// 业务逻辑function showGrace(grace, level, levelForGrace) {for (let i = 0; i < level.length; i++) {if (grace = level[i]) {return levelForGrace[i];}}//如果不存在,那么就是分数很低,返回最后一个return levelForGrace[levelForGrace.length - 1];}showGrace(640,graceForLevel,levelText) // "信用良好"
再来一份配置
let payChanneForChinese = {'cash': '现金','check': '支票','draft': '汇票','zfb': '支付宝','wx_pay': '微信支付',};// 项目配置文件类function getPayChanne(tag,chineseConfig){return chineseConfig[tag];}getPayChanne('cash',payChanneForChinese);
为什么这里推荐配数据置和业务逻辑分离
- 修改配置数据比业务逻辑修改成本更小,风险更低
- 配置数据来源和修改都可以很灵活
- 配置和业务逻辑分离,可以更快的找到需要修改的代码
- 配置数据和业务逻辑可以让代码风格统一
假设信用值有加权
继续使用if-else,这时逻辑升级为二元判断,代码量翻倍增加。
function showGrace(grace, vip) {let level = "";if (vip === true) {if (grace = 700) {level = "信用极好";}// other code} else if (vip === false) {if (grace = 700) {level = "信用极好";}// other code}return level;}
这个时候,可以使用 ES6 中 Map 来处理
const actions = new Map([[{ grace: "700", member: true },// 策略方法() => {console.log("member is vip", 1);},],[{ grace: "550", member: false },() => {/*do sth*/},],//...]);const showGrace = (grace, member) => {let action = [...actions].filter(([key, value]) => key.grace == grace && key.member == member);console.log("action", action);// 匹配 value 后执行 策略方法action.forEach(([key, value]) => value.call(this));};// showGrace("700", true); member is vip
5、搭配设计模式(策略模式)
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
策略模式是oop中最著名的设计模式之一,是对方法行为的抽象,可以归类为行为设计模式 。策略模式定义了一个拥有共同行为的算法族,每个算法都被封装起来,可以互相替换,独立于客户端而变化。
实现特点:一个基于策略模式的程序至少由两部分组成,
第一个部分是一组策略类 Strategies(可变),策略类封装类具体的算法,并负责具体的计算过程。
第二个部分是环境类 Context(不变), Context 接收客户的请求,随后把请求委托给某一个策略类,根据不同参数调用对应的策略函数/对象执行
1、策略模式的使用场景:
- 针对同一问题的多种处理方式,仅仅是具体行为有差别时;
- 需要安全地封装多种同一类型的操作时;
- 同一抽象类有多个子类,而客户端需要使用if-else 或者 switch-case 来选择具体子类时
一个简单的加减乘例子:
interface Compute<T> {computeF(num1: T, num2: T): T;}// 创建策略对象class ComputeAdd implements Compute<number> {public computeF(num1: number, num2: number): number {return num1 + num2;}}class ComputeSub implements Compute<number> {public computeF(num1: number, num2: number): number {return num1 - num2;}}class ComputeMul implements Compute<String> {public computeF(num1: String, num2: String): String {return `${num1} * ${num2}`;}}// 创建行为类class Context {public compute: Compute<any>;public constructor(compute: Compute<any>) {this.compute = compute;}public excuteCompute(num1: number, num2: number): number {return this.compute.computeF(num1, num2);}}let context1 = new Context(new ComputeAdd()).excuteCompute(1, 2);let context2 = new Context(new ComputeSub()).excuteCompute(1, 2);let content3 = new Context(new ComputeMul()).excuteCompute(1, 2);console.log(context1, context2, content3); // 3, -1, 1 + 2
一个角色分工例子
// 策略类(开发人员)var Strategies = {"backend": function(task) {console.log('进行后端任务:', task);},"frontend": function(task) {console.log('进行前端任务:', task);},"testend": function(task) {console.log('进行测试任务:', task);}};// 环境类(开发组长)var Context = function(type, task) {typeof Strategies[type] === 'function' && Strategies[type](task);}Context('backend', '优化服务器缓存');Context('frontend', '优化首页加载速度');Context('testend', '完成系统并发测试');
JavaScript 中,函数作为“一等公民“,也称“一等对象”。JavaScript 中 ”高阶函数“ 应用中,函数可被作为变量或参数进行传递或调用。因此在 JavaScript 中,我们可将算法封装成独立的函数,并将它作为参数传递给另一个函数调用
// 封装独立的函数var backend = function(task) {console.log('进行后端任务:', task);};var frontend = function(task) {console.log('进行前端任务:', task);};var testend = function(task) {console.log('进行测试任务:', task);};// 环境类(开发组长)var Context = function(func, task) {typeof func === 'function' && func(task);}Context(backend, '优化服务器缓存');Context(frontend, '优化首页加载速度');Context(testend, '完成系统并发测试');
三、策略模式的优缺点
优点
- 易于扩展,增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合开放封闭原则
- 避免使用多重条件选择语句,充分体现面向对象设计思想 策略类之间可以自由切换,由于策略类都实现同一个接口,所以使它们之间可以自由切换
- 每个策略类使用一个策略类,符合单一职责原则 客户端与策略算法解耦,两者都依赖于抽象策略接口,符合依赖反转原则
- 客户端不需要知道都有哪些策略类,符合最小知识原则
缺点
- 策略相互独立,因此一些复杂的算法逻辑无法共享,会造成很多的策略类,造成一些资源浪费;
- 如果用户想采用什么策略,必须了解策略的实现,因此所有策略都需向外暴露,这是违背迪米特法则/最少知识原则的,也增加了用户对策略对象的使用成本
6、反思
什么时候用策略模式,解决了什么问题。
思想主要是提取行为差别,同事,对冗余代码进行抽离。就像Promise不能完全避免回调。
1、面向对象编程中,合理拆分,注意粒度**,包括组件设计
KISS 原则( keep it simple,stupid )
2、日常开发模式侧重术,设计模式是道。不知道优化的时候看看文章,多角度🤔
3、不同的场景可能有不同的判断, 动手写代码前先思考。
4、合理抽象,提升复用代码的能力
5、有没有其他方案, 函数式编程 ?
