[TOC]
原理
示例
电影票打折方案(C#实现)
需求
Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:
- 学生凭学生证可享受票价8折优惠
- 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元)
- 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品
- 该系统在将来可能还要根据需要引入新的打折方式
旧的实现方式
1 | class Program |
它至少存在如下三个问题:
OldMovieTicket
类的GetPrice()
方法非常庞大,它包含各种打折算法的实现代码,在代码中出现了较长的if…else…语句,不利于测试和维护- 增加新的打折算法或者对原有打折算法进行修改时必须修改
OldMovieTicket
类的源代码,违反了“开闭原则”,系统的灵活性和可扩展性较差。 - 算法的复用性差,如果在另一个系统(如商场销售管理系统)中需要重用某些打折算法,只能通过对源代码进行复制粘贴来重用,无法单独重用其中的某个或某些算法(重用较为麻烦)。
重构
MovieTicket充当环境类角色,Discount充当抽象策略角色,StudentDiscount、 ChildrenDiscount 和VIPDiscount充当具体策略角色
人员排序(python实现)
有一 Person 类,有年龄(age)、体重(weight)、身高(height)三个属性。现要对 Person 的一组对象进行排序,但并没有确定根据什么规则来排序,有时需要根据年龄进行排序,有时需要根据身高进行排序,有时可能是根据身高和体重的综合情况来排序,还有可能……
1 | class Person: |
测试代码:
1 | def testSortPerson(): |
输出结果: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 | from operator import itemgetter,attrgetter |
为了学习设计模式,我们舍弃了Python本身的语言特性.
另外,Python 语言本身的特性,还是难以实现一些特殊的需求,如要根据身高和体重的综合情况来排序(身高和体重的权重分别是 0.6 和 0.4)。用策略模式就可以很方便地实现,只需要增加一个CompareByHeightAndWeight
的策略类就可以,如下面代码:
1 | class CompareByHeightAndWeight(ICompare): |
策略模式(Python实现)
1 | """http://stackoverflow.com/questions/963965/how-is-this-strategy-pattern-written-in-python-the-sample-in-wikipedia |
Javascript中的策略模式
在JavaScript 语言中,函数也是对象,所以更简单和直接的做法是把strategy直接定义为函数:
计算奖金
很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为S 的人年终奖有4 倍工资,绩效为A 的人年终奖有3 倍工资,而绩效为B 的人年终奖是2 倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖。
1 | var strategies = { |
实现缓动动画
原理
用JavaScript 实现动画效果的原理跟动画片的制作一样,动画片是把一些差距不大的原画以较快的帧数播放,来达到视觉上的动画效果。在JavaScript 中,可以通过连续改变元素的某个CSS属性,比如left、top、background-position 来实现动画效果。下图 就是通过改变节点的background-position,让人物动起来的。
思路和一些准备工作
我们目标是编写一个动画类和一些缓动算法,让小球以各种各样的缓动效果在页面中运动。现在来分析实现这个程序的思路。在运动开始之前,需要提前记录一些有用的信息,至少包括以下信息:
- 动画开始时,小球所在的原始位置;
- 小球移动的目标位置;
- 动画开始时的准确时间点;
- 小球运动持续的时间。
表单校验
假设我们正在编写一个注册的页面,在点击注册按钮之前,有如下几条校验逻辑。
用户名不能为空。
密码长度不能少于 6 位。
手机号码必须符合格式。
第一个版本
1 | <html> |
缺点如下:
registerForm.onsubmit
函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的校验规则。registerForm.onsubmit
函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度校验从 6 改成 8,我们都必须深入registerForm.onsubmit
函数的内部实现,这是违反开 放—封闭原则的。- 算法的复用性差
用策略模式重构
第一步,我们要把这些校验逻辑都封装成策略对象:
1 | var strategies = { |
接下来,我们准备实现Validator
类。Validator
类在这里作为 Context
,负责接收用户的请求 并委托给 strategy
对象。在给出Validator
类的代码之前,有必要提前了解用户是如何向 Validator
类发送请求的,这有助于我们知道如何去编写 Validator
类的代码。代码如下:
1 | var validataFunc = function () { |
从这段代码中可以看到,我们先创建了一个 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 | var Validator = function () { |
使用策略模式重构代码之后,我们仅仅通过“配置”的方式就可以完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地被移植到其他项 目中。
在修改某个校验规则的时候,只需要编写或者改写少量的代码。比如我们想将用户名输入框 的校验规则改成用户名不能少于 4 个字符。可以看到,这时候的修改是毫不费力的。代码如下:
1 | validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' ); |
给某个文本输入框添加多种校验规则
为了让读者把注意力放在策略模式的使用上,目前我们的表单校验实现留有一点小遗憾:一 个文本输入框只能对应一种校验规则,比如,用户名输入框只能校验输入是否为空:
1 | validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' ); |
如果我们既想校验它是否为空,又想校验它输入文本的长度不小于 10 呢?我们期望以这样 的形式进行校验:
1 | validator.add(registerForm.userName, [{ |
实现如下:
1 | <html> |