likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 12

常见的策略模式分析

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

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

误区 | 弊端 | 为什么说网上99%的策略模式都有问题

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

  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呢

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

      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 三个属性决定

    那么我们写一个代码

    let age = 20;
    let region = "cn"
    let school = "A"
    let weacher = "rainy"
    let status = ""
    if (age == 20) {
        if (school == "A") {
            if (region == "cn") {
                status = "high";
                if(weacher=="rainy"){
                    status = "middle"
                }
            }
        } else if(school == "B"){
            status = "middle"
        }
    } else if(age == 22) {
        if (school == "A") {
            status = "middle"
        } else if(school == "B") {
            status = "low"
        }
    }
    

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

    calculateSalary(status, 10)
    

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

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

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

总结 | 改进方向

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

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

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

    {
        age:"20",
        school:"A",
        region:"cn",
        "weacher":"rainy"
    }
    

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

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

实现工程上可用的策略模式

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

key的设计

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

  let a = {
      id:1
  }
  let b = {
      id:1
  }
  console.log(a==b) // false

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

  JSON.stringify(a) ==  JSON.stringify(b)  // true 

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

  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的数据结构是有序的),存数据示例如下

  /**
       * @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)
    

取数据示例如下

  /**
       * @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)!

这样子我们在存取数据用object即使key的顺序不一样也能够取出数据了

发布者订阅者模式的实现

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

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

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

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

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

这样子我们就可以轻松处理当所有策略没有命中的事件和处理报错的事件

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

源码实现

  
  type emitNameType = 'default' | "error";
  
  type IfElseType = {
      // eventbus 
      eventBus?: {
          default: Array<Function>,
          error: 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 array数组
       * @param HashValue 方法
       */
      ActionAdd(HashKey: ObjectType[], HashValue: Function) {
          for(let i = 0 ;i<HashKey.length;i++){
              const orderedMap = new Map();
              const sortedKeys = Object.keys(HashKey[i]).sort();
              for (const key of sortedKeys) {
                  orderedMap.set(key, HashKey[i][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) {
              Fn.bind(that)
          }
          try{
              Fn()
          }catch{
              this.emit("error","报错示例")
          }
              
      }
  }
  
  

对照实验

用原来的方式


let age = 20;
let region = "cn"
let school = "A"
let weacher = "rainy"
let status = ""
if (age == 20) {
    if (school == "A") {
        if (region == "cn") {
            status = "high";
            if(weacher=="rainy"){
                status = "middle"
            }
        }
    } else if(school == "B"){
        status = "middle"
    }
} else if(age == 22) {
    if (school == "A") {
        status = "middle"
    } else if(school == "B") {
        status = "low"
    }
}

const strategies:any = {
    "high": function (workHours: number) {
        console.log("高")
        return workHours * 25
    },
    "middle": function (workHours: number) {
        console.log("中")
        return workHours * 20
    },
    "low": function (workHours: number) {
        console.log("低")
        return workHours * 15
    },
}


strategies(status,10)

用重新设计的策略模式

let res2 = new IfElse({
    eventBus: {
        default: [(e: any) => {
            console.log("触发默认方法:", e)
        }],error:[(e: any) => {
            console.log("触发报错:", e)
        }]
    }
})


let Age20_SchoolA_RegionCN = {age :20,school:"A",region :"cn"}
let Age20_SchoolA_RegionCN_WeacherRainy = {age :20,school:"A",region :"cn",weacher:"rainy"}
let Age20_SchoolB = {age :20,school:"B"}
let AgeNo22_SchoolA = {age :22,school:"A",}
let AgeNo22_SchoolB = {age :22,school:"B"}


res2.ActionAdd([Age20_SchoolA_RegionCN], function(workHours: number) {
    console.log("高")
    return workHours * 25
})
res2.ActionAdd([
    Age20_SchoolB,
    Age20_SchoolA_RegionCN_WeacherRainy,
    AgeNo22_SchoolA
], (workHours: number)=> {
    console.log("中")
    return workHours * 20
})

res2.ActionAdd([AgeNo22_SchoolB], (workHours: number)=> {
    console.log("低")
    return workHours * 15
})

res2.ActionExecute({age :20,school:"A",region :"cn"})  // 高

回顾一下,应该是解决了一些传统策略模式存在的问题

  • 第一点:可扩展性和 语义
  • 第二点:复杂逻辑
  • 第三点:自定义事件

总结

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

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

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