策略模式

[TOC]

原理

示例

电影票打折方案(C#实现)

需求

Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:

  • 学生凭学生证可享受票价8折优惠
  • 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元)
  • 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品
  • 该系统在将来可能还要根据需要引入新的打折方式

旧的实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Program
{
static void Main(string[] args)
{
OldMovieTicket oldMovieTicket = new OldMovieTicket
{
Price = 60.0 //原始票价
};

Console.WriteLine($"原始价为:{oldMovieTicket.Price}");
Console.WriteLine("---------------------------------");

Console.WriteLine($"折后价为:{oldMovieTicket.GetPrice("student")}");//学生票
Console.WriteLine("---------------------------------");

Console.WriteLine($"折后价为:{oldMovieTicket.GetPrice("children")}");//儿童票
Console.WriteLine("---------------------------------");

Console.WriteLine($"折后价为:{oldMovieTicket.GetPrice("vip")}");//vip票
Console.WriteLine("---------------------------------");
}
}

//电影票类
class OldMovieTicket
{
/// <summary>
/// 电影票价格
/// </summary>
public double Price { get; set; }

//计算打折之后的票价
public double GetPrice(string type)
{

//学生票折后票价计算
if (type.Equals("student", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("学生票:");
return Price * 0.8;
}

//儿童票折后票价计算
if (type.Equals("children", StringComparison.OrdinalIgnoreCase)
&& Price >= 20)
{
Console.WriteLine("儿童票:");
return Price - 10;
}

//VIP票折后票价计算
if (type.Equals("vip", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("VIP票:");
Console.WriteLine("增加积分!");
return Price * 0.5;
}

return Price; //如果不满足任何打折要求,则返回原始票价
}
}

它至少存在如下三个问题:

  • OldMovieTicket类的GetPrice()方法非常庞大,它包含各种打折算法的实现代码,在代码中出现了较长的if…else…语句,不利于测试和维护
  • 增加新的打折算法或者对原有打折算法进行修改时必须修改OldMovieTicket类的源代码,违反了“开闭原则”,系统的灵活性和可扩展性较差。
  • 算法的复用性差,如果在另一个系统(如商场销售管理系统)中需要重用某些打折算法,只能通过对源代码进行复制粘贴来重用,无法单独重用其中的某个或某些算法(重用较为麻烦)。

重构

MovieTicket充当环境类角色,Discount充当抽象策略角色,StudentDiscount、 ChildrenDiscount 和VIPDiscount充当具体策略角色

人员排序(python实现)

有一 Person 类,有年龄(age)、体重(weight)、身高(height)三个属性。现要对 Person 的一组对象进行排序,但并没有确定根据什么规则来排序,有时需要根据年龄进行排序,有时需要根据身高进行排序,有时可能是根据身高和体重的综合情况来排序,还有可能……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Person:
"人类"

def __init__(self, name, age, weight, height):
self.name = name
self.age = age
self.weight = weight
self.height = height

def showMysef(self):
print(self.name + " " + str(self.age) + " years old, " +
str(self.weight) + "kg, " + str(self.height) + "m.")


class ICompare:
"比较算法"

def comparable(self, person1, person2):
"person1 > person2 返回值>0,person1 == person2 返回0, person1 < person2 返回值小于0"
pass


class CompareByAge(ICompare):
"通过年龄排序"

def comparable(self, person1, person2):
return person1.age - person2.age


class CompareByHeight(ICompare):
"通过身高进行排序"

def comparable(self, person1, person2):
return person1.height - person2.height


class SortPerson:
"Person的排序类"

def __init__(self, compare):
self.__compare = compare

def sort(self, personList):
"排序算法,这里采用最简单的冒泡排序"
n = len(personList)
for i in range(0, n - 1):
for j in range(0, n - i - 1):
if (self.__compare.comparable(personList[j], personList[j + 1])
> 0):
tmp = personList[j]
personList[j] = personList[j + 1]
personList[j + 1] = tmp
j += 1
i += 1

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def testSortPerson():
personList = [
Person("Tony", 2, 54.5, 0.82),
Person("Jack", 31, 74.5, 1.80),
Person("Nick", 54, 44.5, 1.59),
Person("Eric", 23, 62.0, 1.78),
Person("Helen", 16, 45.7, 1.60)
]
sorter0 = SortPerson(CompareByAge())
sorter0.sort(personList)
print("根据年龄进行排序后的结果:")
for person in personList:
person.showMysef()
print()

sorter1 = SortPerson(CompareByHeight())
sorter1.sort(personList)
print("根据身高进行排序后的结果:")
for person in personList:
person.showMysef()

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
根据年龄进行排序后的结果:
Tony 2 years old, 54.5kg, 0.82m.
Helen 16 years old, 45.7kg, 1.6m.
Eric 23 years old, 62.0kg, 1.78m.
Jack 31 years old, 74.5kg, 1.8m.
Nick 54 years old, 44.5kg, 1.59m.

根据身高进行排序后的结果:
Tony 2 years old, 54.5kg, 0.82m.
Nick 54 years old, 44.5kg, 1.59m.
Helen 16 years old, 45.7kg, 1.6m.
Eric 23 years old, 62.0kg, 1.78m.
Jack 31 years old, 74.5kg, 1.8m.

类图表示如下:

python本身其实也可以实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from operator import itemgetter,attrgetter

def testPersonListInPython():
"用Python的方式对Person进行排序"

personList = [
Person("Tony", 2, 54.5, 0.82),
Person("Jack", 31, 74.5, 1.80),
Person("Nick", 54, 44.5, 1.59),
Person("Eric", 23, 62.0, 1.78),
Person("Helen", 16, 45.7, 1.60)
]

# 使用使用operator模块根据年龄进行排序
print("根据年龄进行排序后的结果:")
sortedPerons = sorted(personList, key = attrgetter('age'))
for person in sortedPerons:
person.showMysef()
print()

print("根据身高进行排序后的结果:")
sortedPerons1 = sorted(personList, key=attrgetter('height'))
for person in sortedPerons1:
person.showMysef()

为了学习设计模式,我们舍弃了Python本身的语言特性.

另外,Python 语言本身的特性,还是难以实现一些特殊的需求,如要根据身高和体重的综合情况来排序(身高和体重的权重分别是 0.6 和 0.4)。用策略模式就可以很方便地实现,只需要增加一个CompareByHeightAndWeight的策略类就可以,如下面代码:

1
2
3
4
5
6
7
class CompareByHeightAndWeight(ICompare):
"根据身高和体重的综合情况来排序(身高和体重的权重分别是0.6和0.4)"

def comparable(self, person1, person2):
value1 = person1.height * 0.6 + person1.weight * 0.4
value2 = person2.height * 0.6 + person2.weight * 0.4
return value1 - value2

策略模式(Python实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
"""http://stackoverflow.com/questions/963965/how-is-this-strategy-pattern-written-in-python-the-sample-in-wikipedia

In most of other languages Strategy pattern is implemented via creating some base strategy interface/abstract class and
subclassing it with a number of concrete strategies (as we can see at http://en.wikipedia.org/wiki/Strategy_pattern),
however Python supports higher-order functions and allows us to have only one class and inject functions into it's
instances, as shown in this example.
"""
import types


class StrategyExample:
def __init__(self, func=None):
self.name = 'Strategy Example 0'
if func is not None:
self.execute = types.MethodType(func, self)

def execute(self):
print(self.name)


def execute_replacement1(self):
print(self.name + ' from execute 1')


def execute_replacement2(self):
print(self.name + ' from execute 2')


if __name__ == '__main__':
strat0 = StrategyExample()

strat1 = StrategyExample(execute_replacement1)
strat1.name = 'Strategy Example 1'

strat2 = StrategyExample(execute_replacement2)
strat2.name = 'Strategy Example 2'

strat0.execute()
strat1.execute()
strat2.execute()

Javascript中的策略模式

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

计算奖金

很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为S 的人年终奖有4 倍工资,绩效为A 的人年终奖有3 倍工资,而绩效为B 的人年终奖是2 倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

实现缓动动画

原理

用JavaScript 实现动画效果的原理跟动画片的制作一样,动画片是把一些差距不大的原画以较快的帧数播放,来达到视觉上的动画效果。在JavaScript 中,可以通过连续改变元素的某个CSS属性,比如left、top、background-position 来实现动画效果。下图 就是通过改变节点的background-position,让人物动起来的。

思路和一些准备工作

我们目标是编写一个动画类和一些缓动算法,让小球以各种各样的缓动效果在页面中运动。现在来分析实现这个程序的思路。在运动开始之前,需要提前记录一些有用的信息,至少包括以下信息:

  • 动画开始时,小球所在的原始位置;
  • 小球移动的目标位置;
  • 动画开始时的准确时间点;
  • 小球运动持续的时间。

表单校验

假设我们正在编写一个注册的页面,在点击注册按钮之前,有如下几条校验逻辑。

  • 用户名不能为空。

  • 密码长度不能少于 6 位。

  • 手机号码必须符合格式。

第一个版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<html>

<body>
<form action="http:// xxx.com/register" id="registerForm" method="post">
请输入用户名:<input type="text" name="userName"/>
请输入密码:<input type="text" name="password"/>
请输入手机号码:<input type="text" name="phoneNumber"/>
<button>提交</button>
</form>
<script>
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function () {
if (registerForm.userName.value === '') {
alert('用户名不能为空');
return false;
}
if (registerForm.password.value.length < 6) {
alert('密码长度不能少于6 位');
return false;
}
if (!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)) {
alert('手机号码格式不正确');
return false;
}
}
</script>
</body>

</html>

缺点如下:

  • registerForm.onsubmit函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的校验规则。
  • registerForm.onsubmit 函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度校验从 6 改成 8,我们都必须深入registerForm.onsubmit函数的内部实现,这是违反开 放—封闭原则的。
  • 算法的复用性差

用策略模式重构

第一步,我们要把这些校验逻辑都封装成策略对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var strategies = {
isNonEmpty: function (value, errorMsg) { // 不为空
if (value === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) { // 限制最小长度
if (value.length < length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) { // 手机号码格式
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
}
};

接下来,我们准备实现Validator 类。Validator类在这里作为 Context,负责接收用户的请求 并委托给 strategy 对象。在给出Validator 类的代码之前,有必要提前了解用户是如何向 Validator类发送请求的,这有助于我们知道如何去编写 Validator 类的代码。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var validataFunc = function () {
var validator = new Validator(); // 创建一个validator 对象
/***************添加一些校验规则****************/
validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空');
validator.add(registerForm.password, 'minLength:6', '密码长度不能少于6 位');
validator.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确');
var errorMsg = validator.start(); // 获得校验结果
return errorMsg; // 返回校验结果
}

var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function () {
var errorMsg = validataFunc(); // 如果errorMsg 有确切的返回值,说明未通过校验
if (errorMsg) {
alert(errorMsg);
return false; // 阻止表单提交
}
};

从这段代码中可以看到,我们先创建了一个 validator 对象,然后通过 validator.add 方法, 往 validator 对象中添加一些校验规则。validator.add 方法接受 3 个参数,validator.add( registerForm.password, 'minLength:6', '密码长度不能少于 6 位' )这句代码说明:

  • registerForm.password 为参与校验的 input 输入框。
  • minLength:6是一个以冒号隔开的字符串。冒号前面的minLength代表客户挑选的strategy对象,冒号后面的数字 6 表示在校验过程中所必需的一些参数。'minLength:6'的意思就是 校验 registerForm.password 这个文本输入框的 value 最小长度为 6。如果这个字符串中不 包含冒号,说明校验过程中不需要额外的参数信息,比如'isNonEmpty'
  • 第 3 个参数是当校验未通过时返回的错误信息。

当我们往 validator 对象里添加完一系列的校验规则之后,会调用 validator.start()方法来 启动校验。如果 validator.start()返回了一个确切的 errorMsg 字符串当作返回值,说明该次校验 没有通过,此时需让 registerForm.onsubmit 方法返回 false 来阻止表单的提交。

最后,是 Validator 类的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Validator = function () {
this.cache = []; // 保存校验规则
};

Validator.prototype.add = function (dom, rule, errorMsg) {
var ary = rule.split(':'); // 把strategy 和参数分开
this.cache.push(function () { // 把校验的步骤用空函数包装起来,并且放入cache
var strategy = ary.shift(); // 用户挑选的strategy
ary.unshift(dom.value); // 把input 的value 添加进参数列表
ary.push(errorMsg); // 把errorMsg 添加进参数列表
return strategies[strategy].apply(dom, ary);
});
};

Validator.prototype.start = function () {
for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
if (msg) { // 如果有确切的返回值,说明校验没有通过
return msg;
}
}
};

使用策略模式重构代码之后,我们仅仅通过“配置”的方式就可以完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地被移植到其他项 目中。

在修改某个校验规则的时候,只需要编写或者改写少量的代码。比如我们想将用户名输入框 的校验规则改成用户名不能少于 4 个字符。可以看到,这时候的修改是毫不费力的。代码如下:

1
2
3
validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' );  
// 改成:
validator.add( registerForm.userName, 'minLength:10', '用户名长度不能小于 10 位' );

给某个文本输入框添加多种校验规则

为了让读者把注意力放在策略模式的使用上,目前我们的表单校验实现留有一点小遗憾:一 个文本输入框只能对应一种校验规则,比如,用户名输入框只能校验输入是否为空:

1
validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' );

如果我们既想校验它是否为空,又想校验它输入文本的长度不小于 10 呢?我们期望以这样 的形式进行校验:

1
2
3
4
5
6
7
validator.add(registerForm.userName, [{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
}, {
strategy: 'minLength:6',
errorMsg: '用户名长度不能小于10 位'
}]);

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<html>

<body>
<form action="http:// xxx.com/register" id="registerForm" method="post">
请输入用户名:<input type="text" name="userName"/>
请输入密码:<input type="text" name="password"/>
请输入手机号码:<input type="text" name="phoneNumber"/>
<button>提交</button>
</form>
<script>
/***********************策略对象**************************/
var strategies = {
isNonEmpty: function (value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.length < length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) {
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
}
};
/***********************Validator 类**************************/
var Validator = function () {
this.cache = [];
};
Validator.prototype.add = function (dom, rules) {
var self = this;
for (var i = 0, rule; rule = rules[i++];) {
(function (rule) {
var strategyAry = rule.strategy.split(':');
var errorMsg = rule.errorMsg;
self.cache.push(function () {
var strategy = strategyAry.shift();
strategyAry.unshift(dom.value);
strategyAry.push(errorMsg);
return strategies[strategy].apply(dom, strategyAry);
});
})(rule)
}
};
Validator.prototype.start = function () {
for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
var errorMsg = validatorFunc();
if (errorMsg) {
return errorMsg;
}
}
};
/***********************客户调用代码**************************/
var registerForm = document.getElementById('registerForm');
var validataFunc = function () {
var validator = new Validator();
validator.add(registerForm.userName, [{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
}, {
strategy: 'minLength:6',
errorMsg: '用户名长度不能小于10 位'
}]);
validator.add(registerForm.password, [{
strategy: 'minLength:6',
errorMsg: '密码长度不能小于6 位'
}]);
var errorMsg = validator.start();
return errorMsg;
}
registerForm.onsubmit = function () {
var errorMsg = validataFunc();
if (errorMsg) {
alert(errorMsg);
return false;
}

};
</script>
</body>

</html>