深入理解JavaScript设计模式之策略模式
深入理解JavaScript设计模式之策略模式
策略模式的起点与终点
某天,你在哪里勤勤恳恳的干活,突然产品经理走到你的旁边说:
“快!年终了,做一个年终奖计算器!”
规则为
- 摸鱼王(S级) 发4倍工资。
- 普通咸鱼(A级) 发3倍工资。
- 卷王(B级) 给个2倍数意思意思得了。
需求初步实现
作为菜鸟的你邪魅一笑,劈里啪啦开始敲键盘,不到两分钟写出了计算年终奖功能:
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倍。
刚提交代码的你,听到需求更改后,天塌了,搁哪里嘀咕:
这需求怎么比我女朋友变脸还快啊!
组合函数重构实现需求
于是你开始重新整理你的代码结构,你学聪明了,从过组合函数重构了逻辑代码
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
函数有可能越来越庞大,而且在系统变化的时候缺乏弹性,不还是和第一版本的差不多嘛! 你很伤心。
策略模式第一次介入
当你把这件事情发到**掘金
沸点或者论坛中,想与同为菜鸟的程序员一起吐槽,得到一点安慰,突然有个大佬的回答让你眼前一亮:
你把算法装进盲盒,让他们互相卷!
你联系到了大佬,屋里哇啦屋里哇啦一顿诉苦,最后大佬指了条明路:使用策略模式!
不明所以的你开始查找资料,扒拉论坛,什么是策略模式,经过一番扒拉与阅览你知道了什么叫做策略模式:
策略模式就是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
可不就是大佬说的把算法装进盲盒,让他们互卷!!
,通过仔细认真学习策略模式,毛瑟顿开,你又劈里啪啦劈里啪啦的重新写了这个功能:
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
直接定义为函数肯定会更简单一点!
于是,又开始了劈里啪啦劈里啪啦一顿敲键盘:
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)); // 输出:80000console.log(calculateBonus("A", 10000)); // 输出:30000
经过改造你写出了一个简洁,清晰,职责更鲜明,可读性高
的功能代码,你不经感叹:
原来代码还可以这样写!到底是谁发明的策略模式,哎呀妈,用了就得劲!太简洁,太清晰,职责更鲜明,可读性高!!
于是,爱上了策略模式
,凡是碰到类似情况你都会第一时间想到策略模式
这个好帮手
解决问题!
多态在策略模式中的体现思考
原始代码中有很多if...else
或switch...case
来判断不同的情况,比如不同等级的员工工资的计算方式不同,使用策略模式后,这些判断被替换为对象之间的"委托"
和“多态”
,让代码更加清晰,“Context”
是策略模式中的一个核心类,它是调用策略的地方,但本身不负责逻辑,每个策略对象代表一种具体的算法,把计算奖金的逻辑分散到不同的策略类中,每个类只处理自己的逻辑。
Context
类只是支持某个策略对象,并将任务交给这个策略去完成,它不知道也不关心具体使用了那个策略用了那个逻辑,只要接口统一就可以调用。
封装是面向对象设计的基本原则之一,每个策略类都封装了自己的实现细节,对外只提供一个统一的接口比如calculateBonus()
方法。
多态是指同一个接口在不同对象上面有不同的行为,所有策略对象都实现了相同的接口,所以可以在运行时候互换使用,而不会影响Context
的结构,Context
内部保存了一个策略对象,只要改变这个策略对象,整个程序的行为就变了,比如按等级计算奖金变成了按固定比例计算奖金,这种灵活性是策略模式的核心优势。
表单校验骚操作名场面
学会了策略模式的第n天,突然产品又又又提出了一个新的需求:
用户名不能为空(总不能叫空气吧)
密码长度≥6(123456警告!)
手机号得是11位(少1位都算耍流氓)
可惜,这个代码交给你你的小学妹去完成,小学妹欣喜若狂,好久没接到这么简单的需求了,立马劈里啪啦劈里啪啦一顿输出,着时有我当时的风范! 不到十分钟,代码完成,我拉取一看!if...else if...else if...else if...if...
好家伙和我当年一毛一样想法,随着需求增多,代码过长,不易扩展,难以维护!我把问题反馈给了小学妹,她和我当时一样愁眉苦脸不知如何是好,于是我搬出了大佬当时的那句话:
你把算法装进盲盒,让他们互相卷!
小迷妹一脸疑惑问我,如何实现,哇咔咔,开始轮到我装逼了! 巴拉巴拉一大堆,又是组合函数重构,又是策略模式引出,又是策略模式定义,又是多态策略模式思考把我当时查阅资料全部说给了小学妹,并且着手了表单校验的需求,小学妹听着一脸错愕与震惊!
我开始劈里啪啦劈里啪啦的敲代码,二十分钟后:
<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函数进行了链式柯里化:
<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设计模式》· 曾探