Skip to content

AHABHGK

策略模式

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

“并且使它们可以相互替换”,这句话在很大程度上是相对于静态类型语言而言的。因为静态类型语言中有类型检查机制,所以各个策略类需要实现同样的接口。当它们的真正类型被隐藏在接口后面时,它们才能被相互替换。而在JavaScript这种“类型模糊”的语言中没有这种困扰,任何对象都可以被替换使用。因此,JavaScript中的“可以相互替换使用”表现为它们具有相同的目标和意图。

计算奖金

员工的奖金与他的绩效有关

第一版

1const calculateBonus = function (performanceLevel, salary) {
2 if (performanceLevel === 'S') {
3 return salary * 4
4 }
5
6 if (performanceLevel === 'A') {
7 return salary * 3
8 }
9
10 if (performanceLevel === 'B') {
11 return salary * 2
12 }
13}
  • 函数体庞大,包含多个判断分支

  • 缺乏弹性,如果修改 S 的倍数,需要在内部修改,违反开放封闭原则

  • 可复用行差

策略模式重构

将算法的使用和算法的实现分开

一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。 第二个部分是环境类 Context,Context 接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明 Context 中要维持对某个策略对象的引用

1// 策略类
2const PerformanceS = function () {}
3PerformanceS.prototype.calculate = function (salary) {
4 return salary * 4
5}
6
7const PerformanceA = function () {}
8PerformanceA.prototype.calculate = function (salary) {
9 return salary * 3
10}
11
12const pPrformanceB = function () {}
13PerformanceB.prototype.calculate = function (salary) {
14 return salary * 2
15}
16
17// 环境类
18const Bonus = function () {
19 this.salary = null
20 this.strategy = null
21}
22
23Bonus.prototype.setSalary = function (salary) {
24 this.salary = salary // 设置员工的原始工资
25};
26
27Bonus.prototype.setStrategy = function (strategy) {
28 this.strategy = strategy // 设置员工绩效等级对应的策略对象
29}
30
31Bonus.prototype.getBonus = function () { // 取得奖金数额
32 return this.strategy.calculate(this.salary) // 把计算奖金的操作委托给对应的策略对象
33}

JavaScript 的策略模式

静态类型中需要通过组合,把策略(算法)封装到类中传到环境类里

JavaScript 函数也是对象,所以更简单和直接的做法是把strategy直接定义为函数

1// 策略
2const strategies = {
3 S(salary) {
4 return salary * 4
5 },
6 A(salary) {
7 return salary * 3
8 },
9 B(salary) {
10 return salary * 2
11 },
12}
13
14// 环境
15const calculateBonus = function (level, salary) {
16 return strategies[level](salary)
17}

有点像用一个对象做映射(map)

多态在策略模式中的体现

Context 并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被各自封装在对象内部。当我们对这些策略对象发出“计算奖金”的请求时,它们会返回各自不同的计算结果,这正是对象多态性的体现,也是“它们可以相互替换”的目的。替换Context中当前保存的策略对象,便能执行不同的算法来得到我们想要的结果。

策略模式实现动画

  • 动画开始时,小球所在的原始位置

  • 小球移动的目标位置

  • 动画开始时的准确时间点

  • 小球运动持续的时间

1// 动画算法
2var tween = {
3 linear: function( t, b, c, d ){
4 return c*t/d + b;
5 },
6 easeIn: function( t, b, c, d ){
7 return c * ( t /= d ) * t + b;
8 },
9 strongEaseIn: function(t, b, c, d){
10 return c * ( t /= d ) * t * t * t * t + b;
11 },
12 strongEaseOut: function(t, b, c, d){
13 return c * ( ( t = t / d - 1) * t * t * t * t + 1 ) + b;
14 },
15 sineaseIn: function( t, b, c, d ){
16 return c * ( t /= d) * t * t + b;
17 },
18 sineaseOut: function(t,b,c,d){
19 return c * ( ( t = t / d - 1) * t * t + 1 ) + b;
20 }
21}
22
23class Animate {
24 constructor(dom) {
25 this.dom = dom // 进行运动的dom节点
26 this.startTime = 0 // 动画开始时间
27 this.startPos = 0 // 动画开始时,dom节点的位置,即dom的初始位置
28 this.endPos = 0 // 动画结束时,dom节点的位置,即dom的目标位置
29 this.direction = null // dom节点需要被改变的css属性名
30 this.easing = null // 缓动算法
31 this.duration = null // 动画持续时间
32 }
33
34 start(direction, endPos, duration, easing) {
35 this.startTime = +new Date; // 动画启动时间
36 this.startPos = this.dom.getBoundingClientRect()[direction]; // dom节点初始位置
37 this.direction = direction; // dom节点需要被改变的CSS属性名
38 this.endPos = endPos; // dom节点目标位置
39 this.duration = duration; // 动画持续事件
40 this.easing = tween[easing]; // 缓动算法
41
42 const timer = setInterval(() => { // 启动定时器,开始执行动画
43 if (this.step() === false){ // 如果动画已结束,则清除定时器
44 clearInterval(timer);
45 }
46 }, 19)
47 }
48
49 step() {
50 const t = +new Date; // 取得当前时间
51 if ( t >= this.startTime + this.duration ){ // (1)
52 this.update( this.endPos ); // 更新小球的CSS属性值
53 return false;
54 }
55 const pos = this.easing( t - this.startTime, this.startPos,
56 this.endPos - this.startPos, this.duration );
57 // pos为小球当前位置
58 this.update( pos ); // 更新小球的CSS属性值
59 }
60
61 update( pos ) {
62 this.dom.style[ this.propertyName ] = pos + 'px'
63 }
64}
65
66
67// test
68var div = document.getElementById( 'div' );
69var animate = new Animate( div );
70
71animate.start( 'left', 500, 1000, 'strongEaseOut' );
72// animate.start( 'top', 1500, 500, 'strongEaseIn' );

广义的“算法”

指一些业务规则

表单验证

1<form action="http:// xxx.com/register" id="registerForm" method="post">
2 请输入用户名:<input type="text" name="userName" />
3 请输入密码:<input type="text" name="password" />
4 请输入手机号码:<input type="text" name="phoneNumber" />
5 <button type="submit">提交</button>
6</form>
  • 用户名不为空

  • 密码长度不小于六位

  • 手机号码必须符合格式

第一版

1var registerForm = document.getElementById('registerForm')
2
3registerForm.onsubmit = function(){
4 if ( registerForm.userName.value === '' ){
5 alert ( '用户名不能为空' )
6 return false
7 }
8 if ( registerForm.password.value.length < 6 ){
9 alert ( '密码长度不能少于6位' )
10 return false
11 }
12 if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value ) ){
13 alert ( '手机号码格式不正确' )
14 return false
15 }
16}

策略模式重构

策略:

1const strategies = {
2 isNonEmpty(spec) {
3 const { value, errorMsg } = spec
4 if (value) {
5 return errorMsg
6 }
7 },
8
9 minLength(spec) {
10 const { value, errorMsg, length } = spec
11 if (value.length < length){
12 return errorMsg
13 }
14 },
15
16 maxLength(spec) {
17 const { value, errorMsg, length } = spec
18 if (value.length > length) {
19 return errorMsg
20 }
21 }
22
23 isMobile(spec){
24 const { value, erroeMsg } = spec
25 if (!/(^1[3|5|8][0-9]{9}$)/.test(value)){
26 return errorMsg
27 }
28 },
29}

环境:

1class Validator {
2 constructor() {
3 this.caches = [] // 保存校验规则
4 }
5
6 add(dom, rules) {
7 const value = dom.value
8 rules.forEach(rule => {
9 this.caches.push(() => {
10 return rule[strategy](rule)
11 })
12 })
13 }
14
15 start() {
16 for (const fn of this.caches) {
17 const errorMsg = fn()
18 if (errorMsg) {
19 return errorMsg
20 }
21 }
22 }
23}

调用:

1const validataFunc = function(){
2 const validator = new Validator()
3
4 // 添加一些校验规则
5 validator.add(registerForm.userName, [
6 { strategy: 'maxLength', errorMsg: '密码长度不大于 10 位', length: 10 },
7 { strategy: 'isNonEmpty', errorMsg: '用户名不能为空' },
8 ])
9 validator.add(registerForm.password, [
10 { strategy: 'minLength', errorMsg: '密码长度不小于 6 位', length: 6 },
11 ])
12 validator.add(registerForm.phoneNumber, [
13 { strategy: 'isMobile', errorMsg: '手机号码格式不正确' },
14 ])
15
16 const errorMsg = validator.start() // 获得校验结果
17 return errorMsg // 返回校验结果
18}
19
20const registerForm = document.querySelector('#registerForm')
21registerForm.addEventListener('submit', e => {
22 const errorMsg = validataFunc() // 如果errorMsg有确切的返回值,说明未通过校验
23 if (errorMsg) {
24 e.preventDefault()
25 }
26})

优缺点

优点:

  • 避免多个分支

  • 符合开放封闭原则

  • 可复用

  • 通过组合和委托代替继承,更轻便

缺点:

  • 程序中会有许多策略对象,但实际上比堆砌在 Context 中好

  • strategy 要向客户暴露它的所有实现,违反最少知识原则

一等函数对象与策略模式

除了将策略封装成对象,还可以将分散在程序中的函数作为策略进行传入

1// 策略
2const S = function (salary) {
3 return salary * 4
4}
5const A = function (salary) {
6 return salary * 3
7}
8const B = function (salary) {
9 return salary * 2
10}
11
12// 环境
13const calculateBonus = function (func, salary) {
14 return func(salary)
15}