为达到一个目的做事情的方法有很多种,比如做工资表,需要计算码农,美工,需求三个人的工资。这时候如果是一个靠谱的人事,一定会有这样一个表:
定下了策略,剩下的就是执行。照着策略的大方向找方法,大致上也不会犯低级错误。
类似见人说人话,见鬼说鬼话的艺术。就是策略模式。在程序设计上表述为:
定义一系列的算法,把它们封装起来。并使之可以互换。
策略模式在程序设计上有极其广泛的应用,表述起来也是非常简单,但实现上就不一定没人做的很好了。以下就举例说明之。
人事在做工资时思路可以简化为:
计算工资方法(calcBonus):
你是谁?
if(码农){
你的工资=2k*1
}
else if(美工){
你的工资=2k*1.5
}
else(产品){
你的工资=2k*2
}
用程序表述起来非常简单。用程序写出来就很恶心了。体现为
关于开放-封闭原则 开放封闭原则(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;
}
// 。。。
有几个表单就写多少次。结果就是阅读这些人写的前端代码会发现他们复制出来的逻辑漫天遍野。
且不论偏见,成熟的表单校验已经很好的运用了策略模式。狭义的策略模式是封装算法。事实上策略模式完全可以封装业务规则。只要这些业务规则指向的目标一致。
假如一个注册页面就是包含用户名/密码/手机号三个字段,判断逻辑是:
应该怎么设计表单校验代码?
你可以把所有的校验方法都封装为策略对象:
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循环你的校验结果。直到第一个校验不通过的作为信息返回。在此不做代码展示。
于是代码又开始没那么好看了,但需求做完才是结果。
但是,策略模式必须向使用者公开实现细节,是违反迪米特原则的。
迪米特法则(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。