专栏首页一Li小麦JavaScript设计模式之策略模式

JavaScript设计模式之策略模式

为达到一个目的做事情的方法有很多种,比如做工资表,需要计算码农,美工,需求三个人的工资。这时候如果是一个靠谱的人事,一定会有这样一个表:

  • 三个岗位的工资计算方法(策略类)
  • 当月的工资Excel(奖金类)

定下了策略,剩下的就是执行。照着策略的大方向找方法,大致上也不会犯低级错误。

类似见人说人话,见鬼说鬼话的艺术。就是策略模式。在程序设计上表述为:

定义一系列的算法,把它们封装起来。并使之可以互换。

策略模式在程序设计上有极其广泛的应用,表述起来也是非常简单,但实现上就不一定没人做的很好了。以下就举例说明之。

从工资表说起

人事在做工资时思路可以简化为:

计算工资方法(calcBonus):
 你是谁?
 if(码农){
 你的工资=2k*1
 }
 else if(美工){
 你的工资=2k*1.5
 }
 else(产品){
 你的工资=2k*2
 }

用程序表述起来非常简单。用程序写出来就很恶心了。体现为

  • if-else太多
  • 复用性差。
  • 缺乏弹性,如果我要增加一个测试岗位,那还得去深入到calcBonus去改,违反了开放-封闭原则

关于开放-封闭原则 开放封闭原则(OCP,Open Closed Principle)是所有面向对象原则的核心。软件设计本身所追求的目标就是封装变化、降低耦合,而开放封闭原则正是对这一目标的最直接体现。其他的设计原则,很多时候是为实现这一目标服务的,例如以Liskov替换原则实现最佳的、正确的继承层次,就能保证不会违反开放封闭原则。


软件实体应该允许扩展,但禁止修改。 ——《面向对象软件构造》 ”对于扩展是开放的。“ 这意味着模块的行为是可以扩展的。当应用程序的需求改变时,我们可以对其模块进行扩展,使其具有满足那些需求变更的新行为。换句话说,我们可以改变模块的功能。 “对于修改是封闭的。“ 对模块行为进行扩展时,不必改动该模块的源代码或二进制代码。 ——《敏捷软件开发:原则、模式与实践》

那么就来重构吧。

const performanceCoder=function(salary){  return salary*1}
const performanceDesigner=function(salary){  return salary*1.5}
const performancePM=function(salary){  return salary*2}
const calcBonus=function(job,salary){  if(job=='coder'){    return performanceCoder(salary));  }  if(job=='designer'){    return performanceDesigner(salary));  }  if(job=='pm'){    return performancePM(salary))  }}
// 使用calcBonus('coder',2000);//输出2000

岗位计算方法算是分离出来了。但是改善很有限。

这可以衍生一道题:

如何优雅地处理if-else?

其实回答的就是策略模式的问题。

/**策略类**/
class performanceCoder={
  calc(salary){
 return salary*1;
 }
}
class PerformanceDesigner{
    calc(salary){
 return salary*1.5;
 }
}
class PerformancePM{
  calc(salary){
 return salary*2;
 }
}
/**工资类**/
class Bonus{
  constructor(){
 this.salary=null;
 this.strategy=null;
 }
 // 设置原始工资
  setSalary(salary){
 this.salary=salary;
 }
 // 工种对应的策略对象
  setStrategy(strategy){
 this.strategy=strategy;
 }
 // 获取奖金数额
  getBonus(){
 return this.strategy.calc(this.salary)
 }
}

整个方法被分成了策略类和奖金类。策略类封装了工资计算方法。而工资类负责配置和把工资计算方法委托给策略类调用。

let coder=new performanceCoder();
let bonus=new Bonus();
bonus.setSalary(2000);
bonus.setStrategy(coder);
bonus.getBonus(); // 2000

在面向对象的实践中,我们实现了一个清晰逻辑策略模式。假如有一天经济不好,基础工资由2000降到了1900。

只需 bonus.setSalary(1900),即可。又比如我增加了测试工种,只需要配置测试工种的策略类,然后就可以调用了。一切都是可插拔的

面向过程的实现

上面的代码已经很好的表现了策略模式的思想。但这是向java学习的写法,不是很能发挥出JavaScript语言的优势。上一章打过不雅的彼比方:JavaScript的优势(或者某些人眼里的劣势)就在于放P不用脱裤子。同理,不用脱裤子的策略模式应该是更加轻松的。

事实上我们可以用键值对的形式定义策略:

let strategies={
  coder:(salary)=>{
 return salary*1;
 },
 // ...
}

也不再需要Bonus类,一行代码搞定。

// 计算方法
const calcBonus=(strategyName,salry)=>(strategies[strategyName](salary));
// 调用:
calcBonus('coder',2000);

表单校验

如果让不熟悉前端的或者兼职前端的程序员来选出JavaScript的用途,结果很可能是这样的:

  • 表单校验

前端入门时也一定会写这样的代码以表示自己学会了js:

if(userName==''){
  alert('用户名不得为空!');
 return false;
}
if(password.length<6){
  alert('密码长度不得少于6位!');
 return false;
}
// 。。。

有几个表单就写多少次。结果就是阅读这些人写的前端代码会发现他们复制出来的逻辑漫天遍野。

且不论偏见,成熟的表单校验已经很好的运用了策略模式。狭义的策略模式是封装算法。事实上策略模式完全可以封装业务规则。只要这些业务规则指向的目标一致。

假如一个注册页面就是包含用户名/密码/手机号三个字段,判断逻辑是:

  • 用户名非空校验
  • 密码长度校验(6位)
  • 手机号格式化校验

应该怎么设计表单校验代码?

你可以把所有的校验方法都封装为策略对象:

let strategies={
  require:(value,msg)=>{
 return value==''?msg:'';
 },
  minLength:(value,msg)=>{
 return value.length<6?msg:'';
 },
  isMobile:(value,msg)=>{
 return !/(^1[3|5|8][0-9]){9}$/.test(value)?msg:'';
 }
}

此时可以思考下你校验架构使用方法,表单校验的用法怎么才算优雅?

我的思路是这样的,构造一个Validator:

const validator=new Validator({field:'#myForm',strategies});
valiadtor.add({name:'userName',rule:'require',msg:'用户名不得为空!'});
valiadtor.add({name:'password',rule:'minLength',msg:'密码长度不得少于6位!'});
valiadtor.add({name:'mobile',rule:'isMobile',msg:'手机号格式不正确!'});
/* 校验详情:
 * 预期返回:[{name:'userName',msg:''},{name:'password',msg:'...'},...]
 */
validator.getValid();
/* 校验全局状态
 * 返回true和false
 */
validator.isValid();

根据可拔插的原则,校验器指定一个范围比如id,strategies可自己配置。有了这个思路就可以开干了。

class Validator{
  construtor({field,strategies}){
 this.strategies=strategies?strategies:{}; //这里可写一套内置的配置
 this.field=document.querySelector(field);
 this.caches=[]; //保存校验规则。
 }
  getVal(name){
 // 可用jq获取this.field下对应name的值,或是虚拟dom的数据
 // 具体由选用的前端框架来分析。此处不展示过程。
 }
  add(rule){
 this.caches.push(rule);
 }
  getValid(){
    let validInfo=this.caches.map((cache)=>{
 const {name,rule,msg}=cache;
 const value=this.getVal(name);
 return {name,msg:this.strategies[rule](value,msg)};
 });
 return validInfo;
 }
  isValid(){
 return this.caches.every((cache)=>(
        cache.msg=='';
 ));
 }
}

这样,你就可以以配置的方式完成表单校验。复用起来也毫不费力。只是还是有一些缺陷。

多种校验规则

策略模式的实现到此可以算结束了。但是需求还没完成。现在修改需求,要求用户名既不能为空,也不能少于6位。

思路是:修改rule的写法,以数组的形式传入。然后在 getValid中while循环你的校验结果。直到第一个校验不通过的作为信息返回。在此不做代码展示。

于是代码又开始没那么好看了,但需求做完才是结果。

小结

  • 策略模式利用委托/组合/多态等思想,可以有效避免多重条件选择语句。
  • 策略模式对开放-封闭原则做了完美的诠释。算法独立封装在strategy类中。使之利于切换,易于理解和扩展。

但是,策略模式必须向使用者公开实现细节,是违反迪米特原则的。

迪米特法则(Law of Demeter)又叫作最少知识原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。英文简写为: LoD.

在JavaScript这种"函数作为一等对象"的语言中,策略模式是隐形的。策略类就是函数。我们可以用高喈函数来封装不同的行为。现在回到工资表的案例:

const coder=(salary)=>{
 return salary*1
}
// ...
const calcBonus=(fn,salary)=>{
 return fn(salary)
}
// 计算码农工资
calcBonus(coder,2000)

显然,改写成这样更像我们熟悉的js。

本文分享自微信公众号 - 一Li小麦(gh_c88159ec1309)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 使用 Whistle 作为 API 服务网关

    最近写了一款 React 的工具,能拉取团队成员 Jira 上的 Task 和 Bug,根据其 Task 的 Efforts 时长和 Bug 的修复时长,计算对...

    猫哥学前班
  • 4-(微信小程序篇)关于本人写的A-H教程里面mqtt.js包使用介绍

    https://www.cnblogs.com/yangfengwu/p/11624796.html

    杨奉武
  • Spring Bean 作用域详解

    在 Spring 中,那些组成应用程序的主体,以及由 Spring IOC 容器所管理的对象,被称之为 bean。简单地讲,bean 就是由 IOC 容器初始化...

    happyJared
  • 学会这几个技巧,让Redis大key问题远离你 原

    个推作为国内第三方推送市场的早期进入者,专注于为开发者提供高效稳定的推送服务,经过9年的积累和发展,服务了包括新浪、滴滴在内的数十万APP。由于我们推送业务对并...

    个推君
  • 快速学会使用Vuex

    半指温柔乐
  • Flutter完整开发实战详解(十八、 神奇的ScrollPhysics与Simulation)

    作为系列文章的第十八篇,本篇将通过 ScrollPhysics 和 Simulation ,带你深入走进 Flutter 的滑动新世界,为你打开 Flutter...

    恋猫
  • Stanford公开课《编译原理》学习笔记(2)递归下降法

    课程里涉及到的内容讲的还是很清楚的,但个别地方有点脱节,建议课下自己配合经典著作《Compilers-priciples, Techniques and Too...

    大史不说话
  • 如何使用JavaScript开发AR(增强现实)移动应用 (一)

    所谓AR(Augmented Reality), 即增强现实,是一种将通过计算机渲染出的虚拟图像与真实世界巧妙融合的手段,背后广泛运用了多媒体、三维建模、实时跟...

    Jerry Wang
  • php 二维数组时间排序实现代码 转

    function arraySort($arr, $keys, $type = 'asc') {

    双面人
  • [Next] 初见next.js

      Next.js 可与 Windows,Mac 和 Linux 一起使用.您只需要在系统上安装 Node.js 即可开始构建 Next.js 应用程序.如果有...

    用户6050340

扫码关注云+社区

领取腾讯云代金券