常见的策略模式分析
当你在网上搜索策略模式的时候,你能看到很多的教程。前几天还有一个冲上热度榜第一的文章,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呢
也就是说,你需要做如下的判断
scssif(你的薪水>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
三个属性决定那么我们写一个代码
inilet 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后,你直接调用
scsscalculateSalary(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
这样子,咱们的策略模式
应该就可以作为一个工具函数放到项目中去了,最后欢迎各路大神留言和讨论~