为什么说网上99%的策略模式都有问题?带你设计一个工程上可用的策略模式

常见的策略模式分析

当你在网上搜索策略模式的时候,你能看到很多的教程。前几天还有一个冲上热度榜第一的文章,juejin.cn/post/727904...

但是你发现当你用了他们的模式之后,放在你的项目中,反而使得你的项目变得更加复杂和臃肿了。甚至于我之前的leader跟我说策略模式不适合用于深层嵌套的语句中。那么他们为什么会出现这种认为用这玩意不如不用的想法呢,在我看来,他们的用法和设计是有大问题的

误区 | 弊端

好了我们来看先来看一下什么是策略模式吧,策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。 简单来说你在用 if else 的时候,你可以用一个key value 数组来进行 替代。我们来看看上面热度榜第一给出的最终优化后的版本吧。

javascript 复制代码
const strategies = {
    "high": function (workHours) {
        return workHours * 25
    },
    "middle": function (workHours) {
        return workHours * 20
    },
    "low": function (workHours) {
        return workHours * 15
    },
}
​
const calculateSalary = function (workerLevel, workHours) {
    return strategies[workerLevel](workHours)
}
console.log(calculateSalary('high', 10)) // 250
console.log(calculateSalary('middle', 10)) // 200
​

先简单讲解一下这段代码吧。strategies 如果是 定义了 3个状态,在3个状态之下,我们对他传入的第二个参数做三个状态做出分别的处理。为什么说这种的策略模式做法用了还不如不用呢?

  • 第一点:可扩展性命名:你看看现在的策略基本是属于一种根本无法扩展的状态。你如果需要添加一个策略,你需要去到strategies这个地方地方去手动添加。

    • 你也许会说,可扩展性那还不简单,搞一个class,设置一个类似于addfunction之类的属性动态添加这个策略里面的策略。不就解决扩展这个问题了,的确,这样子能够解决扩展的问题。但你要知道,我们在使用calculateSalary 的时候,我们需要判断第一个参数是high还是middle还是low呢。诶,这个时候你会发现,tm的我还要判断这个 Salary 是 high还是 middle还是 low呢

      也就是说,你需要做如下的判断

      scss 复制代码
      if(你的薪水>100){
          calculateSalary('high', 10)
      }else if(你的薪水>30){
          calculateSalary('middle', 10)
      }

      额。。。你会发现怎么还是逃不过这个if else。哈哈,我们用设计模式用了一个寂寞(当然聪明的你可能想到了把high | middle | low的判断逻辑抽离出来接着直接传入calculateSalary,这点我们后面再谈)。并且关于这个high,middle,low,万一以后我们需要扩展的话,我们需要怎么命名呢,Much_High 还是 Much_Much_High,这个命名也是一个大问题

  • 第二点:复杂逻辑:你也许会说,即使你说了这么多,那你看,我在工程的确使得这种if/else的判断更加简单了。并且你刚才说的 if else 对于状态的判断,我可以把状态抽离出来。那么还有什么问题吗,有的,那就是对于复杂逻辑的判断

    举一个简单的例子:你的calculateSalary这个函数的第一个参数可能是由 age | region | school 三个属性决定

    那么我们写一个代码

    ini 复制代码
    let age = 20;
    let region = "cn"
    let school = "A"
    ​
    let status = ""
    if(age==20){
        if(school=="A"){
            if(region=="cn"){
                status = "high"
            }    
        }else{
            status = "middle"
        }
    }else{
        if(school=="A"){
             status = "middle"
        }else{
            status = "low"
        }
    }
    ​
    ​

    类似于这种形式你先判断出status后,你直接调用

    scss 复制代码
    calculateSalary(status, 10)

    有没有用到你说的策略模式?用了。这样子行不行?em......if else仍然满天飞,人多少也麻了。这样子跟咱们把方法封装一下塞进去不是一样吗。也就没必要用到我们的策略模式了

  • 第三点:完全没有默认情况的处理:可以看到这类文章普遍没有一个事件兜底的意识,假如该事件没有命中任何一个事件那么我们要怎么处理呢?

  • 第四点:完全没有特殊情况的处理: 我们看到现在我们可以看到,基本上这些函数都是key-value的数值对,假如说我们有一个需求,需要在 value 数值对的函数中 进行错误的错误 或者 在这个函数中的某一个阶段需要向外面的数据进行 交互(进度条 | promise的数组对外进行组装等等)。这些网上的策略模式都是有大问题的

总结 | 改进方向

因此我们可以从哪几个方面去改进这几个点呢,

  • 首先面对着可扩展性,必然是使用class的设计模式,内部通过缓存 key value 数组和添加 类似于 addfunction这类的方法动态的添加function。也就是说,我们需要把策略的执行和定义分离出来

  • 然后怎么处理我们的复杂逻辑。在这一点上,我的想法是通过重新设计key-value来进行解决。常规的策略模式的key是status 这样的一维字符串,这就导致了我们对status进行判断的时候,实际上需要前置的做很多工作,例如我们上面的 关于 calculateSalary这个函数的第一个参数 的 判断,if/else 写了一堆,但是如果我们的key设计成这样呢

    css 复制代码
    {
        age:"20",
        school:"A",
        region:"cn"    
    }

    这种情况下咱们的 value 也就是 status = "high"。这样子是不是很清晰。别的情况我们也可以用这个object进行判断就可以了

  • 然后是我上面说的第三点和第四点,也就是我们关于特殊情况和默认情况的处理。这个时候又要用到我们的发布者/订阅者设计模式了,这里我们可以在内部向外面传递一个事件出来。这样我们就有能力去自定义我们的特殊事件

实现策略模式

碎碎念一些需要注意的地方

key的设计

我们把object作为key,那么我们取出这个key的时候,各个object都是引用类型的,那么就导致这样一个情况

ini 复制代码
let a = {
    id:1
}
let b = {
    id:1
}
console.log(a==b) // false

为了解决这一点我们可以将这个object 进行json.stringify

javascript 复制代码
JSON.stringify(a) ==  JSON.stringify(b)  // true 

但是这样你又发现有一点问题

css 复制代码
let a = {
    name:"xiaoming",
    id:1
}
let b = {
    id:1,
    name:"xiaoming",
}
console.log(JSON.stringify(a) ==  JSON.stringify(b)) // false

我们key的顺序进行换位又有问题,可以看到我们的值其实是一样的但是比较起来又不对了。因此这里可以考虑对这个 object进行排序,但是object本身是没有顺序的,为了解决这一点,我们会采用map这个数据结构进行处理(因为map的数据结构是有序的),存数据示例如下

vbnet 复制代码
/**
     * @des 属性 和 方法
     * @param HashKey 属性object
     * @param HashValue 方法
*/ 
const orderedMap = new Map();
const sortedKeys = Object.keys(HashKey).sort();
for (const key of sortedKeys) {
    orderedMap.set(key, HashKey[key]);
}
let Key = JSON.stringify(Object.fromEntries(orderedMap))
this.MapHash.set(Key, HashValue)
  

取数据示例如下

vbnet 复制代码
/**
     * @des 属性 和 方法
     * @param HashKey 属性object
*/ 
const orderedMap = new Map();
const sortedKeys = Object.keys(HashKey).sort();
for (const key of sortedKeys) {
    orderedMap.set(key, HashKey[key]);
}
let Key =  JSON.stringify(Object.fromEntries(orderedMap));
if (!this.MapHash.get(Key)) {
    this.emit("default","触发默认方法")
    return 
}
let Fn = this.MapHash.get(Key)!

发布者订阅者模式的实现

一个标准的订阅者发布者模式应该包含一个eventbus的调度中心,两个角色(发布者和订阅者)和两个事件on(注册事件) emit(触发事件)

这里我们的eventbus 设计是让用户传入,我们在工具方法中也只有一个emit方法。你也许会问on事件在我们这个示例中是由用户提前定义的,也就是说,触发的事件是eventbus传入的名字.例如下方就是我们会emit 一个default事件给我们的外部,你在外部可以对这个事件做出一系列操作

typescript 复制代码
new IfElse({
    eventBus: {
        default: [(e: any) => {
            console.log("触发默认方法:", e)
        }]
    }
})

在举一个例子,如果你需要对error事件进行监听,你需要在这个class的 内部try catch 捕获异常然后this.emit("error").接着用户传入如下

typescript 复制代码
new IfElse({
    eventBus: {
        default: [(e: any) => {
            console.log("触发默认方法:", e)
        }],
        error:[(e: any) => {
            console.log("触发error方法:", e)
        }]
    }
})

源码地址:github.com/yilaikesi/u...

源码实现

typescript 复制代码
type emitNameType = 'default';
​
type IfElseType = {
    // eventbus 
    eventBus?: {
        default: Array<Function>
    };
}
interface ObjectType {
    id?:boolean,
    isOpen?:boolean,
    [key : string] : any
}
/**
 * @des 要求用户的 action 方法传入
 */
class IfElse {
    MapHash: Map<string, Function>
    config: IfElseType;
    constructor(config: IfElseType) {
        this.config = Object.assign({}, config)
        this.MapHash = new Map()
    }
    /**
     * @des 属性 和 方法
     * @param HashKey 属性object
     * @param HashValue 方法
     */
    ActionAdd(HashKey: ObjectType, HashValue: Function) {
        const orderedMap = new Map();
        const sortedKeys = Object.keys(HashKey).sort();
        for (const key of sortedKeys) {
            orderedMap.set(key, HashKey[key]);
        }
        let Key = JSON.stringify(Object.fromEntries(orderedMap))
        this.MapHash.set(Key, HashValue)
    }
    /**
   * @des 触发某一个事件
   * @param name
   * @param data 给function的值
   */
    emit = (name: emitNameType, data: any) => {
        if (this.config.eventBus) {
            if (this.config.eventBus[name]) {
                this.config.eventBus[name].forEach((element: Function) => {
                    element(data);
                });
            } else {
                throw new Error('没有这个事件');
            }
        }
    };
    ActionExecute(HashKey: Record<string, boolean>, that?: any) {
        const orderedMap = new Map();
        const sortedKeys = Object.keys(HashKey).sort();
        for (const key of sortedKeys) {
            orderedMap.set(key, HashKey[key]);
        }
        let Key =  JSON.stringify(Object.fromEntries(orderedMap));
        if (!this.MapHash.get(Key)) {
            this.emit("default","触发默认方法")
            return 
        }
        let Fn = this.MapHash.get(Key)!
        if (that) {
            return Fn.bind(that)()
        }
        Fn()
    }
}
​
let res2 = new IfElse({
    eventBus: {
        default: [(e: any) => {
            console.log("触发默认方法:", e)
        }]
    }
})
​
let key1:ObjectType = {
    "isCollpse": true,
    "isOpen": true,
}
let key2:ObjectType = {
    "isCollpse": true,
    "isOpen": false,
}
​
​
res2.ActionAdd(key1, function() {
    console.log("key1")
})
res2.ActionAdd(key2, function() {
    console.log("key2")
})
res2.ActionExecute({
    "isOpen": true,
    "isCollpse": true,
})
​
​
​
​

总结

总结一下使用需要注意的地方

  • new class 的时候需要传入eventbus。
  • 新增的时候调用 ActionAdd 然后传入 key - value就可以了
  • 最后使用的时候执行 ActionExecute

这样子,咱们的策略模式应该就可以作为一个工具函数放到项目中去了,最后欢迎各路大神留言和讨论~

相关推荐
mldong5 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排6 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60616 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角7 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!7 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰8 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi9 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen10 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马11 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu11 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss