为什么说网上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

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

相关推荐
JUNAI_Strive_ving5 分钟前
番茄小说逆向爬取
javascript·python
看到请催我学习14 分钟前
如何实现两个标签页之间的通信
javascript·css·typescript·node.js·html5
twins352033 分钟前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js
qiyi.sky1 小时前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
煸橙干儿~~1 小时前
分析JS Crash(进程崩溃)
java·前端·javascript
哪 吒1 小时前
华为OD机试 - 几何平均值最大子数(Python/JS/C/C++ 2024 E卷 200分)
javascript·python·华为od
安冬的码畜日常1 小时前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺
l1x1n02 小时前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
Q_w77422 小时前
一个真实可用的登录界面!
javascript·mysql·php·html5·网站登录
昨天;明天。今天。2 小时前
案例-任务清单
前端·javascript·css