JavaScript设计模式:策略模式详解与实战

作者:纸飞机时间:2026-10-30 03:14:12浏览:662

本文深入解析JavaScript中的策略模式,通过年终奖计算和表单验证两个实例,展示如何将一系列算法封装并相互替换。文章对比了传统面向对象实现与JavaScript函数式实现的差异,帮助开发者消除庞大的if-else逻辑,提升代码的可维护性与扩展性。

策略模式概述

策略的核心在于根据具体情境选择最优方案。例如,规划一次旅行时,我们可以依据时间充裕度、预算限制等实际条件,灵活制定不同的出行路线。

若时间紧迫但预算充足,飞机是最佳选择;若资金有限,大巴或火车更为合适;若预算极低,骑自行车则成为可行方案。在软件开发中,实现同一功能往往存在多种技术路径,例如文件压缩工具既支持Zip算法,也兼容Gzip算法。

策略模式的核心定义在于:将一系列算法分别封装,使其可以互相替换,从而确保算法本身的变更不会影响调用它的客户端代码。

该模式在实际开发中应用广泛,本节将通过年终奖计算的具体案例来展开讲解。

年终奖计算实例

2、年终奖实例

许多企业的年终奖发放规则与员工的基本工资及年度绩效考核等级挂钩。例如,绩效等级为S的员工可获得4倍工资的奖金,等级为A的员工获得3倍工资,而等级为B的员工则获得2倍工资。假设财务部门需要一段代码来自动化计算员工年终奖,我们需要提供相应的解决方案。

1). 最初的代码实现

我们可以编写一个名为calculateBonus的函数来计算奖金。该函数需接收两个参数:员工的基本工资数额及其绩效考核等级。具体实现代码如下:


var calculateBonus = function( performanceLevel, salary ){
 if ( performanceLevel === 'S' ){
 return salary * 4;
 }

 if ( performanceLevel === 'A' ){
 return salary * 3;
 }

 if ( performanceLevel === 'B' ){
 return salary * 2;
 }
};
calculateBonus( 'B', 20000 ); // 输出:40000
calculateBonus( 'S', 6000 ); // 输出:24000

观察上述代码,虽然逻辑简单直观,但存在明显的缺陷。

calculateBonus函数体积庞大,内部充斥着大量的if-else语句,这些语句必须覆盖所有可能的逻辑分支。

该函数缺乏弹性,若未来新增绩效等级C,或将S级奖金系数调整为5倍,我们必须深入修改calculateBonus函数的内部逻辑,这直接违反了开放-封闭原则。

算法复用性极低,若程序其他模块需要计算奖金,只能采取复制粘贴的方式。因此,对这段代码进行重构势在必行。

2). 使用组合函数重构代码

最直观的重构思路是使用组合函数。我们将各种计算逻辑封装为独立的小函数,赋予清晰的命名以表明其对应的算法,同时提高代码在其他模块中的复用性。具体代码如下:


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 ( performanceLevel === 'S' ){
 return performanceS( salary );
 }

 if ( performanceLevel === 'A' ){
 return performanceA( salary );
 }

 if ( performanceLevel === 'B' ){
 return performanceB( salary );
 }

};
calculateBonus( 'A' , 10000 ); // 输出:30000

尽管程序得到了一定程度的优化,但改善幅度有限。核心问题依然存在:calculateBonus函数可能继续膨胀,且在系统需求变更时缺乏足够的弹性。

3). 使用策略模式重构代码

经过深入思考,我们采用策略模式进行重构。策略模式旨在定义一系列算法,将它们分别封装,并实现算法的使用与实现分离。设计模式的核心在于区分不变部分与变化部分,策略模式正是这一思想的体现。

在本例中,算法的使用方式保持不变,即根据特定规则计算奖金数额。然而,算法的具体实现各异且频繁变化,不同绩效等级对应不同的计算逻辑。

基于策略模式的程序至少包含两部分:一是策略类集合,负责封装具体算法并执行计算;二是环境类Context,负责接收客户端请求,并将请求委托给特定的策略对象。为此,Context需持有一个指向具体策略对象的引用。

现在使用策略模式重构前述代码。第一个版本模仿传统面向对象语言的实现方式,将每种绩效的计算规则封装在对应的策略类中:


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;
};

接下来定义奖金管理类Bonus:


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 ); //把计算奖金的操作委托给对应的策略对象
};

在编写最终代码前,回顾策略模式的核心思想:

定义一系列算法,将它们分别封装,并确保它们可以相互替换。

更详细的表述为:定义一系列算法,各自封装为策略类,算法逻辑位于策略类内部的方法中。当客户端向Context发起请求时,Context将请求委托给其中一个策略对象执行计算。

“相互替换”这一特性在静态类型语言中尤为重要,因为类型检查机制要求策略类实现相同的接口,从而隐藏具体类型以实现替换。而在JavaScript这种“类型模糊”的语言中,这种困扰不存在,任何对象均可被替换。因此,JavaScript中的“可替换”体现为对象具有相同的目标和意图。

完成示例剩余代码。首先创建bonus对象并初始化原始数据,如员工基本工资。随后将计算奖金的策略对象传入并保存至bonus对象内部。当调用bonus.getBonus()时,bonus对象自身不执行计算,而是将请求委托给之前保存的策略对象:


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原生实现策略模式的方法。

在5.1节中,strategy对象从各个策略类实例化而来,这是模拟传统面向对象语言的实现方式。实际上,在JavaScript中函数也是对象,因此更简洁直接的做法是将strategy直接定义为函数:


var strategies = {
 "S": function( salary ){
 return salary * 4;
 },
 "A": function( salary ){
 return salary * 3;
 },
 "B": function( salary ){
 return salary * 2;
 }
}; 

同样,Context也不必局限于Bonus类,我们可以继续使用calculateBonus函数作为Context来接收用户请求。经过改造,代码结构更加简洁:


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

策略模式其他应用

3、实例再讲解

通过一个小例子即可一目了然。回顾jQuery中的animate方法。


$( div ).animate( {"left: 200px"}, 1000, 'linear' ); 
//匀速运动
$( div ).animate( {"left: 200px"}, 1000, 'cubic' ); 
//三次方的缓动

这两行代码均使div在1000毫秒内向右移动200像素。其中linear(匀速)和cubic(三次方缓动)即为策略模式的封装体现。

再举一例,许多网页包含即时验证的表单,表单各字段拥有不同的验证规则。

策略模式概述

例如,姓名框需验证非空、敏感词及字符长度。虽然可以用3个if-else解决,但代码的扩展性和维护性堪忧。若表单元素众多,校验情况复杂,可能出现上百个if-else语句。

因此,更好的做法是将每种验证规则用策略模式单独封装。需要特定验证时,只需提供该策略的名称。示例如下:


nameInput.addValidata({
 notNull: true,
 dirtyWords: true,
 maxLength: 30
})
而notNull,maxLength等方法只需要统一的返回true或者false,来表示是否通过了验证。

validataList = {
 notNull: function( value ){
 return value !== '';
 },
 maxLength: function( value, maxLen ){
 return value.length() > maxLen;
 }
}

可见,各种验证规则易于修改和替换。若产品经理建议将字符长度限制改为60个字符,仅需0.5秒即可完成调整。

主要内容介绍至此。

闲聊几句,2015年即将结束,大家的年终奖是否丰厚呢?

希望大家在新的一年里有所收获,通过本文了解策略模式,并理解文中精心准备的两个实例。

作者最新文章