深入理解JavaScript设计模式之策略模式

深入理解JavaScript设计模式之策略模式

策略模式的起点与终点

某天,你在哪里勤勤恳恳的干活,突然产品经理走到你的旁边说:

"快!年终了,做一个年终奖计算器!"

规则为

  • 摸鱼王(S级) 发4倍工资。
  • 普通咸鱼(A级) 发3倍工资。
  • 卷王(B级) 给个2倍数意思意思得了。

需求初步实现

作为菜鸟的你邪魅一笑,劈里啪啦开始敲键盘,不到两分钟写出了计算年终奖功能:

javascript 复制代码
function calculateBonus(level, salary) {
  if (level === 'S') return salary * 4; // 摸鱼之神
   if (level === 'A') return salary * 3; // 普通咸鱼
   if (level === 'B') return salary * 2; // 卷王
 }

结果,刚把代码提交上去,产品经理又又变卦了:

  • 新增C级:加班狂魔0.5倍。
  • 把S级改成5倍。

刚提交代码的你,听到需求更改后,天塌了,搁哪里嘀咕:

这需求怎么比我女朋友变脸还快啊!

组合函数重构实现需求

于是你开始重新整理你的代码结构,你学聪明了,从过组合函数重构了逻辑代码

javascript 复制代码
var performanceS = function( salary ){ 
 return salary * 4; 
}; 
var performanceA = function( salary ){ 
 return salary * 3; 
}; 
var performanceB = function( salary ){ 
 return salary * 2; 
}; 
var calculateBonus = function( performanceLevel, salary ){ 
 if ( level === 'S' ){ 
 return performanceS( salary ); 
 } 
 if ( level === 'A' ){ 
 return performanceA( salary ); 
 } 
 if ( level === 'B' ){ 
 return performanceB( salary ); 
 } 
}; 
calculateBonus( 'A' , 10000 ); // 输出:30000

你的程序得到了一定的改善,你发现这种改善非常有限,依然没有解决最重要的问题:calculateBonus 函数有可能越来越庞大,而且在系统变化的时候缺乏弹性,不还是和第一版本的差不多嘛! 你很伤心。

策略模式第一次介入

当你把这件事情发到**掘金沸点或者论坛中,想与同为菜鸟的程序员一起吐槽,得到一点安慰,突然有个大佬的回答让你眼前一亮:

你把算法装进盲盒,让他们互相卷!

你联系到了大佬,屋里哇啦屋里哇啦一顿诉苦,最后大佬指了条明路:使用策略模式! 不明所以的你开始查找资料,扒拉论坛,什么是策略模式,经过一番扒拉与阅览你知道了什么叫做策略模式:

策略模式就是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

可不就是大佬说的把算法装进盲盒,让他们互卷!!,通过仔细认真学习策略模式,毛瑟顿开,你又劈里啪啦劈里啪啦的重新写了这个功能:

javascript 复制代码
var performanceS = function () {};
performanceS.prototype.calculate = function (salary) {
  return salary * 4;
};

var performanceA = function () {};
performanceA.prototype.calculate = function (salary) {
  return salary * 3;
};

var performanceB = function () {};
performanceB.prototype.calculate = function (salary) {
  return salary * 2;
};

var Bonus = function () {
  this.salary = null; // 原始工资
  this.strategy = null; // 绩效等级对应的策略对象
};
Bonus.prototype.setSalary = function (salary) {
  this.salary = salary; // 设置员工的原始工资
};
Bonus.prototype.setStrategy = function (strategy) {
  this.strategy = strategy; // 设置员工绩效等级对应的策略对象
};
Bonus.prototype.getBonus = function () {
  // 取得奖金数额
  return this.strategy.calculate(this.salary); // 把计算奖金的操作委托给对应的策略对象
};

var bonus = new Bonus();
bonus.setSalary(10000);
bonus.setStrategy(new performanceS()); // 设置策略对象
console.log(bonus.getBonus()); // 输出:40000
bonus.setStrategy(new performanceA()); // 设置策略对象
console.log(bonus.getBonus()); // 输出:30000

一顿输出,你完成了你的第一个策略模式方式的代码编写,但是你又想,既然javaScript中的函数也是对象,干嘛要让strategy对象从各个策略类中创建,你不想javaScript模仿java和其他的面向对象语言实现,有强迫症的你想直接把strategy直接定义为函数肯定会更简单一点! 于是,又开始了劈里啪啦劈里啪啦一顿敲键盘:

javascript 复制代码
var strategies = {
   S: function (salary) {
     return salary * 4;
   },
   A: function (salary) {
     return salary * 3;
   },
   B: function (salary) {
     return salary * 2;
   },
 };
 var calculateBonus = function (level, salary) {
   return strategies[level](salary);
 };
 console.log(calculateBonus("S", 20000)); // 输出:80000
 console.log(calculateBonus("A", 10000)); // 输出:30000

经过改造你写出了一个简洁,清晰,职责更鲜明,可读性高的功能代码,你不经感叹:

原来代码还可以这样写!到底是谁发明的策略模式,哎呀妈,用了就得劲!太简洁,太清晰,职责更鲜明,可读性高!!

于是,爱上了策略模式,凡是碰到类似情况你都会第一时间想到策略模式这个好帮手解决问题!

多态在策略模式中的体现思考

原始代码中有很多if...elseswitch...case来判断不同的情况,比如不同等级的员工工资的计算方式不同,使用策略模式后,这些判断被替换为对象之间的"委托""多态",让代码更加清晰,"Context"是策略模式中的一个核心类,它是调用策略的地方,但本身不负责逻辑,每个策略对象代表一种具体的算法,把计算奖金的逻辑分散到不同的策略类中,每个类只处理自己的逻辑。 Context类只是支持某个策略对象,并将任务交给这个策略去完成,它不知道也不关心具体使用了那个策略用了那个逻辑,只要接口统一就可以调用。 封装是面向对象设计的基本原则之一,每个策略类都封装了自己的实现细节,对外只提供一个统一的接口比如calculateBonus()方法。 多态是指同一个接口在不同对象上面有不同的行为,所有策略对象都实现了相同的接口,所以可以在运行时候互换使用,而不会影响Context的结构,Context内部保存了一个策略对象,只要改变这个策略对象,整个程序的行为就变了,比如按等级计算奖金变成了按固定比例计算奖金,这种灵活性是策略模式的核心优势。

表单校验骚操作名场面

学会了策略模式的第n天,突然产品又又又提出了一个新的需求:

用户名不能为空(总不能叫空气吧) 密码长度≥6(123456警告!) 手机号得是11位(少1位都算耍流氓)

可惜,这个代码交给你你的小学妹去完成,小学妹欣喜若狂,好久没接到这么简单的需求了,立马劈里啪啦劈里啪啦一顿输出,着时有我当时的风范! 不到十分钟,代码完成,我拉取一看!if...else if...else if...else if...if... 好家伙和我当年一毛一样想法,随着需求增多,代码过长,不易扩展,难以维护!我把问题反馈给了小学妹,她和我当时一样愁眉苦脸不知如何是好,于是我搬出了大佬当时的那句话:

你把算法装进盲盒,让他们互相卷!

小迷妹一脸疑惑问我,如何实现,哇咔咔,开始轮到我装逼了! 巴拉巴拉一大堆,又是组合函数重构,又是策略模式引出,又是策略模式定义,又是多态策略模式思考把我当时查阅资料全部说给了小学妹,并且着手了表单校验的需求,小学妹听着一脸错愕与震惊! 我开始劈里啪啦劈里啪啦的敲代码,二十分钟后:

javascript 复制代码
<html>
  <body>
    <form action="http:// xxx.com/register" id="registerForm" method="post">
      请输入用户名:<input type="text" name="userName"/ >
      请输入手机号码:<input type="text" name="phoneNumber"/ >
      <button>提交</button>
    </form>
    <script>
      const validators = {
        isNonEmpty: (value, msg) => (value === "" ? msg : null),
        minLength: (value, length, msg) => (value.length < length ? msg : null),
        isMobile: (value, msg) => (!/^1[3-9]\d{9}$/.test(value) ? msg : null),
      };
      // 验证器:一个无情的甩锅机器
      class Validator {
        constructor() {
          this.rules = []; // 装规则的垃圾桶
        }
        add(dom, rule, msg) {
          const [strategy, params] = rule.split(":");
          this.rules.push(() =>
            validators[strategy](dom.value, ...(params ? [params] : []), msg)
          );
        }
        validate() {
          return this.rules.map((rule) => rule()).find((msg) => msg);
        }
      }
      // 使用:学妹你看!代码多整齐!
      const registerForm = document.getElementById("registerForm");
      const validator = new Validator();
      validator.add(registerForm.userName, "isNonEmpty", "用户名呢亲!");
      validator.add(registerForm.phoneNumber, "isNonEmpty", "手机号呢亲!");
      validator.add(registerForm.phoneNumber, "isMobile", "手机号是乱编的吧?");
      // 提交时:一键甩锅
      registerForm.onsubmit = () => {
        const error = validator.validate();
        if (error) {
          alert(error);
          return false;
        } // 优雅の打脸
      };
    </script>
  </body>
</html>

ohhhh,完成了验证需求,看效果!: 小迷妹一脸崇拜,一脸震惊! 我得意的开始吹牛逼了,其实你如果嫌validator.add一步一步加的太麻烦你可以将add改造成柯里化方法,你在小学妹的崇拜下,你开始劈里啪啦劈里啪啦将add函数进行了链式柯里化:

javascript 复制代码
<html>
  <body>
    <form action="http:// xxx.com/register" id="registerForm" method="post">
      请输入用户名:<input type="text" name="userName"/ >
      请输入手机号码:<input type="text" name="phoneNumber"/ >
      <button>提交</button>
    </form>
    <script>
      const validators = {
        isNonEmpty: (value, msg) => (value === "" ? msg : null),
        minLength: (value, length, msg) => (value.length < length ? msg : null),
        isMobile: (value, msg) => (!/^1[3-9]\d{9}$/.test(value) ? msg : null),
      };
      // 验证器:一个无情的甩锅机器
      class Validator {
        constructor() {
          this.rules = []; // 装规则的垃圾桶
        }
        add(dom, rule, msg) {
          const [strategy, params] = rule.split(":");
          this.rules.push(() =>
            validators[strategy](dom.value, ...(params ? [params] : []), msg)
          );

          // 返回一个函数,用于继续添加规则
          return (nextDom, nextRule, nextMsg) => {
            if (!nextRule || !nextMsg) {
              throw new Error("参数不完整,请提供 DOM、规则和提示信息");
            }
            const [nextStrategy, nextProps] = nextRule.split(":");
            this.rules.push(() =>
              validators[nextStrategy](
                nextDom.value,
                ...(nextProps ? [nextProps] : []),
                nextMsg
              )
            );
            return this.add.bind(this); // 继续返回自己以便无限链式调用
          };
        }

        validate() {
          return this.rules.map((rule) => rule()).find((msg) => msg);
        }
      }
      const inputName = document.getElementById("registerForm");
      // 学妹你看!代码多整齐!
      const validator = new Validator();
      validator.add(inputName.userName, "isNonEmpty", "用户名呢亲!")(
        inputName.userName,
        "minLength:6",
        "用户名太短了亲"
      );
      validator.add(registerForm.phoneNumber, "isNonEmpty", "手机号呢亲!")(
        registerForm.phoneNumber,
        "isMobile",
        "手机号是乱编的吧?"
      );

      // 提交时:一键甩锅
      registerForm.onsubmit = () => {
        const error = validator.validate();
        if (error) {
          alert(error); // 优雅の打脸
          return false;
        }
      };
    </script>
  </body>
</html>

效果: 哇咔咔,逐渐在小学妹一声声夸赞声中迷失了自我

策略模式再好用也是有优缺点

优点
  • 策略模式利用组合,委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  • 策略模式提供了对开放封闭原则的完美支持,将算法独立发封装再strategy中,使得他们容易切换,容易理解,用以扩展
  • 策略模式中的算法可以服用再系统的其他地方,宠儿避免重复的复制粘贴工作。
  • 策略模式中理由组合和委托来让Context拥有执行算法的能力,这也是继承的一种轻便的替代的方案。
缺点
  • 使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在 Context 中要好。
  • 要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点,这样才能选择一个合适的 strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机、火车、自行车等方案的细节。此时strategy要向客户暴露它的所有实现,这是违反最少知识原则的。

结尾暴击

策略模式翻译成人话就是:

"在JS里,策略模式就是------函数:你直接报我身份证号得了!"

下次产品经理让你改需求,请优雅转身:

"稍等,我换个策略~" (然后默默打开strategies.js深藏功与名)

PS:改需求时,建议给产品经理买杯咖啡(加满策略模式!!!!)

总结

设计模式不是"炫技",而是"沉淀",希望通过阅读和学习《JavaScript设计模式》和实践中,在显示业务需求开发中写出更具有可维护性,可扩展性的代码。

致敬------ 《JavaScript设计模式》· 曾探

相关推荐
小满zs5 小时前
Zustand 第五章(订阅)
前端·react.js
涵信6 小时前
第一节 基础核心概念-TypeScript与JavaScript的核心区别
前端·javascript·typescript
谢尔登6 小时前
【React】常用的状态管理库比对
前端·spring·react.js
编程乐学(Arfan开发工程师)6 小时前
56、原生组件注入-原生注解与Spring方式注入
java·前端·后端·spring·tensorflow·bug·lua
小公主6 小时前
JavaScript 柯里化完全指南:闭包 + 手写 curry,一步步拆解原理
前端·javascript
姑苏洛言8 小时前
如何解决答题小程序大小超过2M的问题
前端
GISer_Jing9 小时前
JWT授权token前端存储策略
前端·javascript·面试
开开心心就好9 小时前
电脑扩展屏幕工具
java·开发语言·前端·电脑·php·excel·batch
拉不动的猪9 小时前
es6常见数组、对象中的整合与拆解
前端·javascript·面试
GISer_Jing9 小时前
Vue Router知识框架以及面试高频问题详解
前端·vue.js·面试