很多时候会发现自己在写代码的时候写了一坨if else 语句使得自己的代码看起来很丑,随着业务量的增大,代码变得很难维护,之前想到能替换if else的只有switch,其实效果并没有明显的提升,现在在看设计模式方面的知识,发现两种设计模式能够解决分支判断的臃肿问题。
状态模式
使用场景
大家都知道超级玛丽的游戏吧,玛丽要吃蘑菇,他就要挑起,顶出墙壁里的蘑菇;玛丽想到悬崖的另一边,他就要跳起;玛丽想躲避被前面的乌龟咬到,他就要开枪打死乌龟;前面飞过炮弹,玛丽就要蹲下躲避;时间不够了,就要加速奔跑···
如果这个游戏要用if或者switch条件判断,显得有些疲惫,如果使用状态模式将‘跳跃’、‘开枪’、‘蹲下’和‘奔跑’作为一个个的状态来开发,之后在遇到不同情况的时候直接使用状态,思路将变得清晰。
代码实现
首先创建一个状态对象,内部保存状态变量,然后内部封装好每种动作对应的状态,最后状态对象返回一个接口对象,它可以对内部的状态修改或者调用。
代码如下:
/* * 超级玛丽里面的状态: 跳跃、开枪、蹲下、奔跑等 */ const MarryState = function() { // 内部状态私有变量 let _currentState = {}; // 动作与状态方法映射 const states = { jump() { // 跳跃 console.log('jump'); }, move() { // 移动 console.log('move'); }, shoot() { // 射击 console.log('shoot'); }, squat() { // 蹲下 console.log('squat'); } }; // 动作控制类 const Action = { // 改变状态方法 changeState() { // 组合动作通过传递多个参数实现 let arg = arguments; // 重置内部状态 _currentState = {}; if(arg.length) { // 遍历动作 for(let i = 0,len = arg.length; i < len; i++) { // 向内部状态中添加动作 _currentState[arg[i]] = true; } } // 返回动作控制类 return this; }, // 执行动作 goes() { console.log('触发一次动作'); // 遍历内部状态保存的动作 for(let i in _currentState) { // 如果改动作存在则执行 states[i] && states[i](); } return this; } }; // 返回接口方法 change、goes return { change: Action.changeState, goes: Action.goes } };
使用方式:
// 创建一个超级玛丽 const marry = new MarryState(); marry .change('jump','shoot') // 添加跳跃与射击动作 .goes() // 执行动作 .goes() // 执行动作 .change('shoot') // 添加射击动作 .goes(); // 执行动作
可以发现,状态模式中的状态可以连续使用。
原理和优点
原理:将条件判断的不同结果转化为对象的内部状态,作为对象内部的私有变量。
好处:当我们需要增加、修改、调用、删除某种状态方法时就会很容易,也方便了我们对状态对象中内部状态的管理。
用途:状态模式是为了解决程序中臃肿的分支判断语句问题,将每个分支转化为一种状态独立出来,方便每种状态的管理又不至于每次执行时遍历所有分支。
总之,状态模式的最终目的是简化分支判断流程。
策略模式
使用场景
商品的促销活动。在圣诞节,一部分商品5折出售,一部分商品8折出售,一部分商品9折出售,等到元旦,普通用户满100减30,高级VIP用户满100减50···
如果这种情况用if或switch来写将是一件很费时费力的事情。
而且对于圣诞节或者元宵节,当天的一种商品只有一种促销策略,根本不用关心其他的促销状态。
代码实现
首先要将这些促销算法封装在一个策略对象内,然后对每种商品的策略调用时,直接对策略对象中的算法调用即可,为方便我们的管理与使用,我们需要返回一个调用接口对象来实现对策略算法对调用。
代码如下:
/* * 价格策略对象 */ const PriceStrategy = function () { // 内部算法对象 const strategy = { // 100 返 30 return30(price) { // parseInt可通过~~、|等运算符替换,要注意此时price要在[-2147483648,2147483647]之间 // +price 转换为数字类型 return +price + parseInt(price / 100) * 30; }, // 100 返 50 return50(price) { return +price + parseInt(price / 100) * 50; }, // 9 折 percent90(price) { // JavaScript 在处理小数乘除法有bug,故运算前转化为整数 return price * 100 * 90 / 10000; }, // 8 折 percent80(price) { // JavaScript 在处理小数乘除法有bug,故运算前转化为整数 return price * 100 * 80 / 10000; }, // 5 折 percent50(price) { // JavaScript 在处理小数乘除法有bug,故运算前转化为整数 return price * 100 * 50 / 10000; } }; // 策略算法调用接口 return function (algorithm, price) { // 如果算法存在,则调用算法,否则返回false return stragtegy[algorithm] && stragtegy[algorithm](price); } }();
使用方式:
const price = PriceStrategy('return50', '343.20');
console.log(price);
表单验证中的策略模式
代码如下:
/* * 表单正则验证侧罗对象 */ const InputStrategy = function () { const strategy = { // 是否为空 notNull(value) { return /s+/.test(value) }, // 是否是一个数字 number(value) { return /^[0-9]+(.[0-9]+)?$/.test(value); }, // 是否为本地电话 phone(value) { // 例:010-94837837 或 0310-8899766 return /^d{3}-d{8}$|^d{4}-d{7}$/.test(value); } }; return { // 验证接口 type 算法 value 表单值 check(type, value) { // 去除首尾空白符 value = value.replace(/^s+|s+$/g, ''); return strategy[type] ? strategy[type](value) : '没有该类型等检测方法'; }, // 添加策略 addStrategy(type, fn) { strategy[type] = fn; } } };
可以发现,表单验证返回的接口中添加了一个添加策略接口。因为已有的策略即使再多,有时候也不能满足其他工程师的需求,这样就可以增强策略对象的拓展性。
// 拓展 可以延伸算法 InputStrategy.addStrategy('nickname', function (value) { return /^[a-zA-Z]w{3,7}$/.test(value); });
原理和优点
策略模式:将定义的一组算法封装起来,使其相互之间可以替换。
策略模式不需要管理状态、状态间没有依赖关系、策略之间可以相互替换、在策略对象内部保存的是相互独立的一些算法。
策略模式使得算法脱离与模块逻辑而独立管理,使我们可以专心研发算法,而不必受模块逻辑所约束。
其他
设计模式并不是很高深不可理解的一门学问,只是根据经验把复杂的业务逻辑整理清楚,使之更容易操作化。
根据不同的业务逻辑选择不同的设计模式有助于简化代码,也有助于代码的解耦,使得代码更加有效和可维护。
参考书籍:《JavaScript设计模式》