从条件判断到策略模式

复杂判断的优雅写法

在实际的业务场景开发中,常用的判断有

if-else 流,同时兼有 switch-case , 三目运算符。

1、 if-esle

首先看看基本的逻辑处理。

  1. /* 复杂度中规中矩,语义化差,且不常注释 */
  2. const GradeType = (a) => {
  3. if (a == 1) {
  4. console.log('A')
  5. } else if (a == 2) {
  6. console.log('B')
  7. } else if (a == 3) {
  8. console.log('B')
  9. } else if (a == 4) {
  10. console.log('C')
  11. }
  12. /* and more */
  13. }

问题:

  1. 1、条件判断多,易遗漏,后期不好修改
  2. 2、要依次判断每个分支条件

2、借助 switch

  1. const GradeTypeCase = (a) => {
  2. switch (a) {
  3. case 1:
  4. console.log("A");
  5. break;
  6. case 2:
  7. case 3:
  8. console.log("B");
  9. break;
  10. case 4:
  11. console.log("C");
  12. break;
  13. default:
  14. break;
  15. }
  16. };

switch 的问题在于,灵活度不够,针对值为 常量 可使用

3、三元表达式

对较简单的使用三元表达式

  1. // 三元表达式
  2. ExamPass = price >= 60 ? "pass" : "fail";
  3. // if else 判断
  4. if (price >= 60) {
  5. return "pass";
  6. } else {
  7. return "fail";
  8. }
  1. // 不滥用三目运算符
  2. if (!aup || !bup) {
  3. return a === doc
  4. ? -1
  5. : b === doc
  6. ? 1
  7. : aup
  8. ? -1
  9. : bup
  10. ? 1
  11. : sortInput
  12. ? indexOf.call(sortInput, a) - indexOf.call(sortInput, b)
  13. : 0;
  14. }

可选链 ?.

  1. let user = {}; // user 没有 address 属性
  2. alert( user.address && user.address.street && user.address.street.name );
  3. // undefined(不报错)
  4. // 使用可选链
  5. alert( user?.address?.street ); // undefined(不报错)

4、表驱动 (配数据置和业务逻辑)

假设芝麻信用的场景

  1. function showGrace(grace) {
  2. let level='';
  3. if(grace=700){
  4. level='信用极好'
  5. }
  6. // other code
  7. else if(grace=550){
  8. level='信用中等'
  9. }
  10. else{
  11. level='信用较差'
  12. }
  13. return level;
  14. }
  1. ⚡️ 需求变更了
  • grace 有变
  • 展示文字变更
    暴露问题, 不够灵活,需要一个个条件去修改

来做个小修改

  1. // 配置项
  2. let graceForLevel = [700, 650, 600, 550];
  3. let levelText = ["信用极好", "信用优秀", "信用良好", "信用中等", "信用较差"];
  4. // 业务逻辑
  5. function showGrace(grace, level, levelForGrace) {
  6. for (let i = 0; i < level.length; i++) {
  7. if (grace = level[i]) {
  8. return levelForGrace[i];
  9. }
  10. }
  11. //如果不存在,那么就是分数很低,返回最后一个
  12. return levelForGrace[levelForGrace.length - 1];
  13. }
  14. showGrace(640,graceForLevel,levelText) // "信用良好"

再来一份配置

  1. let payChanneForChinese = {
  2. 'cash': '现金',
  3. 'check': '支票',
  4. 'draft': '汇票',
  5. 'zfb': '支付宝',
  6. 'wx_pay': '微信支付',
  7. };
  8. // 项目配置文件类
  9. function getPayChanne(tag,chineseConfig){
  10. return chineseConfig[tag];
  11. }
  12. getPayChanne('cash',payChanneForChinese);

为什么这里推荐配数据置和业务逻辑分离

  1. 修改配置数据比业务逻辑修改成本更小,风险更低
  2. 配置数据来源和修改都可以很灵活
  3. 配置和业务逻辑分离,可以更快的找到需要修改的代码
  4. 配置数据和业务逻辑可以让代码风格统一

假设信用值有加权

继续使用if-else,这时逻辑升级为二元判断,代码量翻倍增加。

  1. function showGrace(grace, vip) {
  2. let level = "";
  3. if (vip === true) {
  4. if (grace = 700) {
  5. level = "信用极好";
  6. }
  7. // other code
  8. } else if (vip === false) {
  9. if (grace = 700) {
  10. level = "信用极好";
  11. }
  12. // other code
  13. }
  14. return level;
  15. }

这个时候,可以使用 ES6Map 来处理

  1. const actions = new Map([
  2. [
  3. { grace: "700", member: true },
  4. // 策略方法
  5. () => {
  6. console.log("member is vip", 1);
  7. },
  8. ],
  9. [
  10. { grace: "550", member: false },
  11. () => {
  12. /*do sth*/
  13. },
  14. ],
  15. //...
  16. ]);
  17. const showGrace = (grace, member) => {
  18. let action = [...actions].filter(
  19. ([key, value]) => key.grace == grace && key.member == member
  20. );
  21. console.log("action", action);
  22. // 匹配 value 后执行 策略方法
  23. action.forEach(([key, value]) => value.call(this));
  24. };
  25. // showGrace("700", true); member is vip

5、搭配设计模式(策略模式)

设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。

策略模式是oop中最著名的设计模式之一,是对方法行为的抽象,可以归类为行为设计模式 。策略模式定义了一个拥有共同行为的算法族,每个算法都被封装起来,可以互相替换,独立于客户端而变化。

实现特点:一个基于策略模式的程序至少由两部分组成,

第一个部分是一组策略类 Strategies(可变),策略类封装类具体的算法,并负责具体的计算过程。

第二个部分是环境类 Context(不变), Context 接收客户的请求,随后把请求委托给某一个策略类,根据不同参数调用对应的策略函数/对象执行

1、策略模式的使用场景:
  • 针对同一问题的多种处理方式,仅仅是具体行为有差别时;
  • 需要安全地封装多种同一类型的操作时;
  • 同一抽象类有多个子类,而客户端需要使用if-else 或者 switch-case 来选择具体子类时

一个简单的加减乘例子:

  1. interface Compute<T> {
  2. computeF(num1: T, num2: T): T;
  3. }
  4. // 创建策略对象
  5. class ComputeAdd implements Compute<number> {
  6. public computeF(num1: number, num2: number): number {
  7. return num1 + num2;
  8. }
  9. }
  10. class ComputeSub implements Compute<number> {
  11. public computeF(num1: number, num2: number): number {
  12. return num1 - num2;
  13. }
  14. }
  15. class ComputeMul implements Compute<String> {
  16. public computeF(num1: String, num2: String): String {
  17. return `${num1} * ${num2}`;
  18. }
  19. }
  20. // 创建行为类
  21. class Context {
  22. public compute: Compute<any>;
  23. public constructor(compute: Compute<any>) {
  24. this.compute = compute;
  25. }
  26. public excuteCompute(num1: number, num2: number): number {
  27. return this.compute.computeF(num1, num2);
  28. }
  29. }
  30. let context1 = new Context(new ComputeAdd()).excuteCompute(1, 2);
  31. let context2 = new Context(new ComputeSub()).excuteCompute(1, 2);
  32. let content3 = new Context(new ComputeMul()).excuteCompute(1, 2);
  33. console.log(context1, context2, content3); // 3, -1, 1 + 2

一个角色分工例子

  1. // 策略类(开发人员)
  2. var Strategies = {
  3. "backend": function(task) {
  4. console.log('进行后端任务:', task);
  5. },
  6. "frontend": function(task) {
  7. console.log('进行前端任务:', task);
  8. },
  9. "testend": function(task) {
  10. console.log('进行测试任务:', task);
  11. }
  12. };
  13. // 环境类(开发组长)
  14. var Context = function(type, task) {
  15. typeof Strategies[type] === 'function' && Strategies[type](task);
  16. }
  17. Context('backend', '优化服务器缓存');
  18. Context('frontend', '优化首页加载速度');
  19. Context('testend', '完成系统并发测试');

JavaScript 中,函数作为“一等公民“,也称“一等对象”。JavaScript 中 ”高阶函数“ 应用中,函数可被作为变量或参数进行传递或调用。因此在 JavaScript 中,我们可将算法封装成独立的函数,并将它作为参数传递给另一个函数调用

  1. // 封装独立的函数
  2. var backend = function(task) {
  3. console.log('进行后端任务:', task);
  4. };
  5. var frontend = function(task) {
  6. console.log('进行前端任务:', task);
  7. };
  8. var testend = function(task) {
  9. console.log('进行测试任务:', task);
  10. };
  11. // 环境类(开发组长)
  12. var Context = function(func, task) {
  13. typeof func === 'function' && func(task);
  14. }
  15. Context(backend, '优化服务器缓存');
  16. Context(frontend, '优化首页加载速度');
  17. Context(testend, '完成系统并发测试');

三、策略模式的优缺点

优点
  • 易于扩展,增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合开放封闭原则
  • 避免使用多重条件选择语句,充分体现面向对象设计思想 策略类之间可以自由切换,由于策略类都实现同一个接口,所以使它们之间可以自由切换
  • 每个策略类使用一个策略类,符合单一职责原则 客户端与策略算法解耦,两者都依赖于抽象策略接口,符合依赖反转原则
  • 客户端不需要知道都有哪些策略类,符合最小知识原则

缺点
  • 策略相互独立,因此一些复杂的算法逻辑无法共享,会造成很多的策略类,造成一些资源浪费;
  • 如果用户想采用什么策略,必须了解策略的实现,因此所有策略都需向外暴露,这是违背迪米特法则/最少知识原则的,也增加了用户对策略对象的使用成本

6、反思

什么时候用策略模式,解决了什么问题。
思想主要是提取行为差别,同事,对冗余代码进行抽离。就像Promise不能完全避免回调。

1、面向对象编程中,合理拆分,注意粒度**,包括组件设计
KISS 原则( keep it simple,stupid )
2、日常开发模式侧重术,设计模式是道。不知道优化的时候看看文章,多角度🤔
3、不同的场景可能有不同的判断, 动手写代码前先思考。
4、合理抽象,提升复用代码的能力
5、有没有其他方案, 函数式编程 ?