本篇是面向对象设计系列文章的第四篇,讲解的是设计模式中的7个比较常见的行为型模式(按照本文讲解顺序排列):
- 模板方法模式
- 策略模式
- 责任链模式
- 状态模式
- 命令模式
- 观察者模式
- 中介者模式
一. 模板方法模式
定义
在模板模式(Template Method Pattern)中,定义一个操作中的算法的框架,而将一些步骤的执行延迟到子类中,使得子类可以在不改变算法的结构的前提下即可重新定义该算法的某些特定步骤。
适用场景
通常一个算法需要几个执行步骤来实现,而有时我们需要定义几种执行步骤一致,但是却可能在某个步骤的实现略有差异的算法。也就是说我们既需要复用实现相同的步骤,也可以通过在某个步骤的不同实现来灵活扩展出更多不同的算法。
在这种场景下,我们可以使用模板方法模式:定义好一个算法的框架,在父类实现可以复用的算法步骤,而将需要扩展和修改其他步骤的任务推迟给子类进行。
现在我们清楚了模板方法模式的适用场景,下面看一下这个模式的成员和类图。
成员与类图
成员
模板方法模式的成员除了客户端以外,只有两个成员:
- 算法类(Algorithm):算法类负责声明算法接口,算法步骤接口。并实现可复用的算法步骤接口,且将需要子类实现的接口暴露出来。
- 具体算法类(Concrete Algorithm):具体算法类负责实现算法类声明的算法步骤接口。
有些参考资料定义这两个成员为
Abstract Class
和Concrete Class
。
下面通过类图来看一下命令模式各个成员之间的关系:
模式类图
由上图可以看出,Algorithm
的excute
方法是算法接口,它在内部调用了三个步骤方法:step1
,step2
,step3
。而step2
是未暴露在外部的,因为这个步骤是需要各个子类复用的。因此Algorithm
只将step1
和step3
暴露了出来以供子类来调用。
代码示例
场景概述
模拟一个制作三种热饮的场景:热美式咖啡,热拿铁,热茶。
场景分析
这三种热饮的制作步骤是一致的,都是三个步骤:
- 步骤一:准备热水
- 步骤二:加入主成分
- 步骤三:加入辅助成分(也可以不加,看具体热饮的种类)
虽然制作步骤是一致的,但是不同种类的热饮在每一步可能是不同的:咖啡和茶叶主成分是咖啡粉和茶叶;而辅助成分:美式咖啡和茶叶可以不添加,而拿铁还需添加牛奶。
而第一步是相同的:准备热水。
根据上面对模板方法模式的介绍,像这样算法步骤相同,算法步骤里的实现可能相同或不同的场景我们可以使用模板方法模式。下面我们看一下如何用代码来模拟该场景。
代码实现
首先我们创建算法类HotDrink
:
1 | //================== HotDrink.h ================== |
HotDrink
向外部暴露了一个制作过程的接口makingProcess
,这个接口内部调用了热饮的所有制作步骤方法:
1 | - (void)makingProcess{ |
HotDrink
只向外暴露了这三个步骤中的两个需要子类按照自己方式实现的接口:
1 | //添加主成分 |
因为热饮的第一步都是一致的(准备热水),所以第一步骤的接口没有暴露出来给子类实现,而是直接在当前类实现了,这也就是模板方法的一个可以复用代码的优点。
OK,我们现在创建好了算法类,那么根据上面的需求,我们接着创建三个具体算法类:
HotDrinkTea
: 热茶HotDrinkLatte
: 热拿铁HotDrinkAmericano
: 热美式
1 | //================== HotDrinkTea.h ================== |
热茶在addMainMaterial
步骤里面是添加了茶叶,而在addIngredients
步骤没有做任何事情(这里先假定是纯的茶叶)。
类似地,我们看一下两种热咖啡的实现。首先是热拿铁HotDrinkLatte
:
1 | //================== HotDrinkLatte.h ================== |
热拿铁在addMainMaterial
步骤里面是添加了咖啡粉,而在addIngredients
步骤添加了牛奶。
下面再看一下热美式HotDrinkAmericano
:
1 | //================== HotDrinkAmericano.h ================== |
热美式在addMainMaterial
步骤里面是添加了咖啡粉,而在addIngredients
步骤没有做任何事,因为美式就是纯的咖啡,理论上除了水和咖啡不需要添加任何其他东西。
到现在三种热饮类创建好了,我们现在分别制作这三种热饮,并看一下日至输出:
1 | ===== Begin to making HotDrinkTea ===== |
上面的日至输出准确无误地反映了我们所定义的这三种热饮制作过程:
- 热茶:准备热水 + 茶叶
- 热拿铁:准备热水 + 咖啡 + 牛奶
- 热美式:准备热水 + 咖啡
下面看一下上面代码对应的类图。
代码对应的类图
优点
- 复用性高:将相同的代码放在父类中,而不同的部分则由子类实现
- 扩展性高:可以通过创建不同的子类来扩展不同的算法
- 符合开闭原则:可变与不可变的部分分离,而且不同的可变部分(子类)也是相互分离的,所以符合了开闭原则
缺点
- 导致类的个数增加:对于每一个算法实现都需要一个子类,如果实现过多的话会导致类的个数增加
- 由继承关系导致的缺点:如果父类需要增加或减少它的行为,则所有的子类都需要同步修改一次
iOS SDK 和 JDK中的应用
- 在 iOS SDK 中,我们可以重写
UIView
的drawRect:
方法可以自定义绘图,是模板方法模式的一种实践。 - 在JDK中,
java.lang.Runnable
是使用JDK的经典场景:Runnable
接口可以作为抽象的命令,而实现了Runnable的线程即是具体的命令。
二. 策略模式
定义
策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。
适用场景
有时候在实现某一个功能的时可能会有多个方案:我们需要让系统可以动态灵活地更换方案;而且也能够让开发者方便地增加新的方案或删除旧的方案。
如果我们将所有的方案硬编码在同一个类中,那么在今后修改,添加,删除某个方案的时候就会改动原有类,这是违反开闭原则的。
其实我们可以定义一些独立的类来封装不同的解决方案,每一个类封装一个具体的方案,这些不同的方案就是我们所说的策略。而且我们可以用一个抽象的策略类来保证这些策略的一致性,这就是策略模式的设计方案。
现在我们清楚了策略模式的适用场景,下面看一下策略模式的成员和类图。
成员与类图
成员
策略模式除了客户端之外共有三个成员:
- 环境类(Context):环境类内部持有一个具体策略类的实例,这个实例就是当前的策略,可以供客户端使用
- 抽象策略类(Strategy):抽象策略类声明具体策略类需要实现的接口,这个接口同时也是提供给客户端调用的接口
- 具体策略类(Concrete Strategy):具体策略类实现抽象策略类声明的接口,每个具体策略类都有自己独有的实现方式,即代表不同策略
下面我们通过类图来看一下各个成员之间的关系。
模式类图
代码示例
场景概述
模拟一个两个整数可以随意替换加减乘除算法的场景。
场景分析
在该场景中,传入的两个整数参数是不变的,但是对于这两个整数的具体操作可以灵活切换,那么我们可以使用策略模式:将每个操作(算法)封装起来,在需要替换的时候将Context
类持有的具体策略实例更新即可。
代码实现
首先我们定义好抽象策略类和具体策略类:
因为是针对两个整数的操作,所以在抽象策略类中,我们只需定义一个传入两个整数的接口即可。
抽象策略类TwoIntOperation
:
1 | //================== TwoIntOperation.h ================== |
接着我们根据加减乘除四种运算,来分别定义四个具体策略类:
加法TwoIntOperationAdd
:
1 | //================== TwoIntOperationAdd.h ================== |
减法TwoIntOperationSubstract
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//================== TwoIntOperationSubstract.h ==================
@interface TwoIntOperationSubstract : TwoIntOperation
@end
//================== TwoIntOperationSubstract.m ==================
@implementation TwoIntOperationSubstract
- (int)operationOfInt1:(int)int1 int2:(int)int2{
NSLog(@"==== Substract ====");
return int1 - int2;
}
@end
乘法TwoIntOperationMultiply
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//================== TwoIntOperationMultiply.h ==================
@interface TwoIntOperationMultiply : TwoIntOperation
@end
//================== TwoIntOperationMultiply.m ==================
@implementation TwoIntOperationMultiply
- (int)operationOfInt1:(int)int1 int2:(int)int2{
NSLog(@"==== multiply ====");
return int1 * int2;
}
@end
除法TwoIntOperationDivision
:
1 | //================== TwoIntOperationDivision.h ================== |
现在关于算法的类都声明好了,我们最后声明一下 Context
类:
1 | //================== Context.h ================== |
Context
类在构造器(init方法)注入了一个具体策略实例并持有它,而且Context
也提供了set
方法,让外部注入进来具体策略类的实例。
而策略的具体执行是通过Context
的接口excuteOperationOfInt1:int2
。这个接口是提供给客户端调用的;而且在它的内部其实调用的是当前持有的策略实例的执行策略的方法。
所以如果想使用哪种策略,只要将具体策略的实例传入到Context
实例即可。
现在所有的类都定义好了,下面我们看一下具体如何使用:
1 | int int1 = 6; |
看一下日至输出:1
2
3
4
5
6
7
8
9[13431:1238320] int1: 6 int2: 3
[13431:1238320] ==== adding ====
[13431:1238320] result of adding : 9
[13431:1238320] ==== multiply ====
[13431:1238320] result of multiplying : 18
[13431:1238320] ==== Substract ====
[13431:1238320] result of substracting : 3
[13431:1238320] ==== division ====
[13431:1238320] result dividing : 2
在上面的例子中,首先我们要使用加法,所以 实例化了加法策略类并传入到了Context
类的构造器中。
而后续的乘法,减法,除法的更换,则是分别将它们的策略实例传入到了Context
的set方法中,并执行即可。
下面看一下上面代码对应的类图。
代码对应的类图
优点
- 策略模式遵循开闭原则,用户可以在不修改原有系统的前提下选择和更换算法
- 避免使用多重条件判断
- 可以灵活地增加新的算法或行为
- 提高算法和策略的安全性:可以封装策略的具体实现,调用者只需要知道不同策略之间的区别就可以
缺点
- 客户端必须知道当前所有的具体策略类,而且需要自行决定使用哪一个策略类
- 如果可选的方案过多,会导致策略类数量激增。
iOS SDK 和 JDK中的应用
- JDK中的
Comparator
是策略模式的实现,可以使用不同的子类,也就是具体策略来解决不同的需求。
三. 责任链模式
定义
责任链模式(Chain of Responsibility Pattern):为请求创建了一个接收者对象的链,每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
适用场景
在处理某个请求的时候,解决策略因条件不同而不同。这时,相对于使用if-else
来区分不同的条件和对应的解决策略,我们可以使用责任链模式,将不同条件和对应的解决策略封装到一个类中,即不同的处理者。然后将这些处理者组成责任链,在当前处理者无法处理或不符合当前条件时,将请求传递给下一个处理者。
现在我们清楚了责任链模式的适用场景,下面看一下责任链模式的成员和类图。
成员与类图
成员
责任链模式的结构比较简单,不包括客户端只有两个成员:
- 处理者(Handler):处理者定义处理请求的接口
- 具体处理者(Concrete Handler): 具体处理者实现处理者声明的接口,负责处理请求
模式类图
代码示例
场景概述
模拟一个 ATM 取现金的场景:ATM机器有50,20,10面值的纸币,根据用户需要提取的现金金额来输出纸币张数最少的等价金额的纸币。
比如用户需要取130元,则ATM需要输出2张50面额的纸币,1张20面额的纸币,1张10面额的纸币;而不是6张20面额的纸币加1张10面额的纸币。
场景分析
显然,为了输出最少张数的纸币,ATM在计算的时候是从面额最大的纸币开始计算的。
如果不使用责任链模式,我们可能会写一个do-while
循环,在循环里面再根据纸币的面额在做if-else
判断,不断去尝试直到将面额除尽(没有余数)。但是如果未来面额的数值发生变化,或者添加新的面额的纸币的话,我们还需要更改判断条件或增加if-else
语句,这显然违反了开闭原则。
但是如果使用责任链模式,我们将每个面值的纸币当做责任链中的一个处理者(节点,node),自成一类,单独做处理。然后将这些处理者按照顺序连接起来(50,20,10),按照顺序对用户输入的数值进行处理即可。
这样做的好处是,如果以后修改面值或添加一种新的面值,我们只需要修改其中某一个处理者或者新建一个处理者类,再重新插入到责任链的合适的位置即可。
下面我们看一下如何用代码来模拟该场景。
代码实现
首先创建抽象处理者DispenseChainNode
:
1 | //================== DispenseChainNode.h ================== |
DispenseChainNode
是责任链节点,也就是具体处理者的父类,它持有DispenseChainNode
的实例,用来保存当前节点的下一个节点。这个下一个节点的实例是通过setNextChainNode:
方法注入进来的
而且,DispenseChainNode
遵循<DispenseProtocol>
协议,这个协议只有一个方法,就是dispense:
方法,每个节点都实现这个方法来对输入的金额做处理。(dispense 单词的意思是分配,分发)
现在我们根据需求,创建具体处理者,也就是针对50,20,10面额的具体处理者:
50面额的具体处理者: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//================== DispenseChainNodeFor50Yuan.h ==================
@interface DispenseChainNodeFor50Yuan : DispenseChainNode
@end
//================== DispenseChainNodeFor50Yuan.m ==================
@implementation DispenseChainNodeFor50Yuan
- (void)dispense:(int)amount{
int unit = 50;
if (amount >= unit) {
int count = amount/unit;
int remainder = amount % unit;
NSLog(@"Dispensing %d of %d",count,unit);
if (remainder != 0) {
[_nextChainNode dispense:remainder];
}
}else{
[_nextChainNode dispense:amount];
}
}
@end
20面额的具体处理者: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//================== DispenseChainNodeFor20Yuan.h ==================
@interface DispenseChainNodeFor20Yuan : DispenseChainNode
@end
//================== DispenseChainNodeFor20Yuan.m ==================
@implementation DispenseChainNodeFor20Yuan
- (void)dispense:(int)amount{
int unit = 20;
if (amount >= unit) {
int count = amount/unit;
int remainder = amount % unit;
NSLog(@"Dispensing %d of %d",count,unit);
if (remainder != 0) {
[_nextChainNode dispense:remainder];
}
}else{
[_nextChainNode dispense:amount];
}
}
@end
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//================== DispenseChainNodeFor10Yuan.h ==================
@interface DispenseChainNodeFor10Yuan : DispenseChainNode
@end
//================== DispenseChainNodeFor10Yuan.m ==================
@implementation DispenseChainNodeFor10Yuan
- (void)dispense:(int)amount{
int unit = 10;
if (amount >= unit) {
int count = amount/unit;
int remainder = amount % unit;
NSLog(@"Dispensing %d of %d",count,unit);
if (remainder != 0) {
[_nextChainNode dispense:remainder];
}
}else{
[_nextChainNode dispense:amount];
}
}
@end
上面三个具体处理者在dispense:
方法的处理都是类似的:
首先查看当前值是否大于面额
- 如果大于面额
- 将当前值除以当前面额
- 如果没有余数,则停止,不作处理
- 如果有余数,则继续将当前值传递给下一个具体处理者(责任链的下一个节点)
- 将当前值除以当前面额
- 如果小于面额:将当前值传递给下一个具体处理者(责任链的下一个节点)
现在我们创建好了三个具体处理者,我们再创建一个ATM类来把这些节点串起来:
1 | //================== ATMDispenseChain.h ================== |
ATMDispenseChain
这个类在初始化的时候就将三个具体处理者并按照50,20,10的顺序连接起来,并持有一个DispenseChainNode
的指针指向当前的具体处理者(也就是责任链的第一个节点,面额50的具体处理者,因为面额的处理是从50开始的)。
OK,现在我们把三个具体处理者都封装好了,可以看一下如何使用:
1 | ATMDispenseChain *atm = [[ATMDispenseChain alloc] init]; |
创建ATMDispenseChain
的实例后,分别传入一些数值来看一下处理的结果:
1 | ================================== |
从日志的输出可以看出,我们的责任链处理是没有问题的,针对每个不同的数值,ATMDispenseChain
实例都作出了最正确的结果。
需要注意的是,该代码示例中的责任链类(
ATMDispenseChain
)并没有在上述责任链模式的成员中。不过此处不必做过多纠结,我们在这里只是在业务上稍微多做一点处理罢了。其实也完全可以不封装这些节点,直接逐个调用setNextChainNode:
方法组装责任链,然后将任务交给第一个处理者即可。
需求完成了,是否可以做个重构?
我们回去看一下这三个具体处理者在dispense:
方法的处理是非常相似的,他们的区别只有处理的面额数值的不同:而我们其实是创建了针对这三个面值的类,并将面值(50,20,10)硬编码在了这三个类中。这样做是有缺点的,因为如果后面的面额大小变了,或者增加或者减少面额的话我们会修改这些类或添加删除这些类(即使这也比不使用责任链模式的if-else
要好一些)。
因此我们可以不创建这些与面额值硬编码的具体处理类,而是在初始化的时候直接将面额值注入到构造方法里面即可!这样一来,我们可以随意调整和修改面额了。下面我们做一下这个重构:
首先删除掉三个具体处理者DispenseChainNodeFor50Yuan
,DispenseChainNodeFor20Yuan
,DispenseChainNodeFor10Yuan
。
接着在DispenseChainNode
添加传入面额值的初始化方法以及面额值的成员变量:
1 | //================== ADispenseChainNode.h ================== |
我们给DispenseChainNode
添加了initWithDispenseValue:
方法后,就可以根据需求随意生成不同面额的具体处理者了。
接着我们思考一下之前的ATMDispenseChain
可以做哪些改变?
既然DispenseChainNode
可以根据不同的面额值生成处理不同面额的具体处理者实例,那么对于串联多个具体处理者的类ATMDispenseChain
是不是也可以添加一个注入面额数组的初始化方法呢?比如输入[50,20,10]
的数组就可以生成50,20,10面额的具体处理者了;而且数组是有序的,传入数组的元素顺序就可以是责任链中节点的顺序。
思路有了,我们看一下具体实现:
1 | //================== ATMDispenseChain.m ================== |
重构后的ATMDispenseChain
类新增了initWithDispenseNodeValues:
方法,需要从外部传入面额值的数组。在这个方法里面根据传入的数组构造了整条责任链。
而在dispense:
方法里面则是从责任链的第一个节点来处理面额,并在方法最前面取最小面额的值来做边界处理。
OK,到现在处理者类和责任链类都创建好了,我们看一下如何使用:
1 | NSArray *dispenseNodeValues = @[@(100),@(50),@(20),@(10)]; |
是不是感觉简洁多了?我们只需要传入一个面额值的数组即可构造出整条责任链并直接使用。来看一下日至输出:
1 | ================================== |
从日志的输出结果上看,我们重构后的责任链方案没有问题。
下面看一下上面代码对应的类图。
代码对应的类图
重构前:
重构后:
优点
- 处理者之间的责任分离,处理者只要处理好自己的逻辑即可
- 方便修改每个处理者的处理逻辑,也方便删除或者添加处理者,或者改变责任链中处理者的顺序。
缺点
- 因为需要在责任链上传递责任,直到找到合适的对象来处理,所以可能会导致处理的延迟。因此在延迟不允许过高的场景下不适合使用责任链模式。
iOS SDK 和 JDK中的应用
- iOS SDK中的响应者链就是责任链模式的实践:如果当前视图无法响应则传递给下一层级视图。
servlet
中的Filter
可以组成FilterChain
,是责任链模式的一种实践。
四. 状态模式
定义
在状态模式(State Pattern):允许一个对象在其内部状态改变时,改变它的行为。
适用场景
一个对象存在多个状态,不同状态下的行为会有不同,而且状态之间可以相互转换。
如果我们通过if else
来判断对象的状态,那么代码中会包含大量与对象状态有关的条件语句,而且在添加,删除和更改这些状态的时候回比较麻烦;而如果使用状态模式。将状态对象分散到不同的类中,则可以消除 if...else
等条件选择语句。
现在我们清楚了状态模式的适用场景,下面看一下状态模式的成员和类图。
成员与类图
成员
状态模式一共只有四个成员:
- 环境类(Context):环境类引用了具体状态的实例。环境类持有的具体状态就是当前的状态,可以通过 set 方法将状态实例注入到环境类中。
- 抽象状态类(State):抽象状态类声明具体状态类需要实现的接口。
- 具体状态类(Concrete State):具体状态类实现抽象状态类声明的接口。
下面通过类图来看一下各个成员之间的关系:
模式类图
代码示例
场景概述
模拟一个程序员一天的生活,他有四个状态:
- 醒着
- 睡觉中
- 写代码中
- 吃饭中
看这几个状态应该是个非常爱写代码的程序员 ^ ^
场景分析
这个程序员有四个状态,但是有些状态之间是无法切换的:比如从睡觉是无法切换到写代码的(因为需要切换到醒着,然后才能到写代码);从吃饭中是无法切换到醒着的,因为已经醒着了。
如果我们不使用状态模式,在切换状态的时候可能会写不少if-else
判断,而且随着状态的增多,这些分支会变得更多,难以维护。
而如果我们使用状态模式,则可以将每个状态封装到一个类中,便于管理;而且在增加或减少状态时也会很方便。
下面我们看一下如何用代码来模拟该场景。
代码实现
首先我们定义状态类:
1 | //================== State.h ================== |
状态类持有一个coder
,也就是程序员的实例,并遵循了ActionProtocol
:
1 | //================== ActionProtocol.h ================== |
ActionProtocol
定义了程序员的一些动作,这些动作是程序员的日常活动,也是触发状态切换的动作,因此State
也需要遵循这个协议,因为它的子类需要实现这些操作。
接下来我们看一下State
的子类,根据上面说的四种状态,我们定义下面四个状态子类:
StateAwake
: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//================== StateAwake.h ==================
@interface StateAwake : State
@end
@implementation StateAwake
- (void)wakeUp{
NSLog(@"Already awake, can not change state to awake again");
}
- (void)startCoding{
NSLog(@"Change state from awake to coding");
[_coder setState:(State *)[_coder stateCoding]];
}
- (void)startEating{
NSLog(@"Change state from awake to eating");
[_coder setState:(State *)[_coder stateEating]];
}
- (void)fallAsleep{
NSLog(@"Change state from awake to sleeping");
[_coder setState:(State *)[_coder stateSleeping]];
}
@end
StateSleeping
: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//================== StateSleeping.h ==================
@interface StateSleeping : State
@end
//================== StateSleeping.m ==================
@implementation StateSleeping
- (void)wakeUp{
NSLog(@"Change state from sleeping to awake");
[_coder setState:(State *)[_coder stateAwake]];
}
- (void)startCoding{
NSLog(@"Already sleeping, can not change state to coding");
}
- (void)startEating{
NSLog(@"Already sleeping, can change state to eating");
}
- (void)fallAsleep{
NSLog(@"Already sleeping, can not change state to sleeping again");
}
@end
StateEating
: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//================== StateEating.h ==================
@interface StateEating : State
@end
//================== StateEating.m ==================
@implementation StateEating
- (void)wakeUp{
NSLog(@"Already awake, can not change state to awake again");
}
- (void)startCoding{
NSLog(@"New idea came out! change state from eating to coding");
[_coder setState:(State *)[_coder stateCoding]];
}
- (void)startEating{
NSLog(@"Already eating, can not change state to eating again");
}
- (void)fallAsleep{
NSLog(@"Too tired, change state from eating to sleeping");
[_coder setState:(State *)[_coder stateSleeping]];
}
@end
“StateCoding”: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//================== StateCoding.h ==================
@interface StateCoding : State
@end
//================== StateCoding.m ==================
@implementation StateCoding
- (void)wakeUp{
NSLog(@"Already awake, can not change state to awake again");
}
- (void)startCoding{
NSLog(@"Already coding, can not change state to coding again");
}
- (void)startEating{
NSLog(@"Too hungry, change state from coding to eating");
[_coder setState:(State *)[_coder stateEating]];
}
- (void)fallAsleep{
NSLog(@"Too tired, change state from coding to sleeping");
[_coder setState:(State *)[_coder stateSleeping]];
}
@end
从上面的类可以看出,在有些状态之间的转换是失效的,有些是可以的。
比如相同状态的切换是无效的;从 sleeping
无法切换到coding
,但是反过来可以,因为可能写代码累了就直接睡了。
下面我们看一下程序员类的实现:
1 | //================== Coder.h ================== |
从上面的代码我们可以看到,程序员类持有一个当前的状态的实例,在初始化后默认的状态为awake
,并对外提供一个setState:
的方法来切换状态。而且在初始化方法里,我们实例化了所有的状态,目的是在切换状态中时使用,详见具体状态类的方法:
1 | - (void)startEating{ |
上面这段代码有点绕,可能需要多看几遍源码才能理解(这里面
[_coder stateEating]
是调用了coder
的一个get
方法,返回了stateEating
这个实例)。
最后,在程序员的动作方法里面,实际上调用的是当前状态对应的方法(这也就是为何程序员类和状态类都要遵循ActionProtocol
的原因)。
这样,我们的状态类,状态子类,程序员类都声明好了。我们看一下如何使用:
1 | Coder *coder = [[Coder alloc] init]; |
在上面的代码里,我们实例化了一个程序员类,接着不断调用一些触发状态改变的方法。我们把每次状态切换的日至输出注释到了代码右侧,可以看到在一些状态的切换是不允许的:
- 比如从上到下的第一个
[coder wakeUp]
:因为程序员对象初始化后默认是awake
状态,所以无法切换到相同的状态 - 比如从上到下的第一个
[coder startEating]
:在睡觉时是无法直接切换到eating
状态;而在后面wake以后,再执行[coder startEating]
就成功了。
从上面的例子可以看出,使用状态模式不需要去写if-else
,而且如果今后想添加一个状态,只需要再创建一个状态子类,并在新的状态子类添加好对所有状态的处理,并在之前的状态子类中添加上对新状态的处理即可。即便我们修改了之前定义好的状态子类,但是这样也总比使用庞大的if-else
要方便多。
下面看一下上面代码对应的类图。
代码对应的类图
优点
- 把各种状态的转换逻辑,分布到不同的类中,减少相互间的依赖。
缺点
- 增加新的状态类需要修改状态转换的源码,而且增加新的行为也要修改原来的状态类(前提是新的行为和原来的状态有关系)。
- 过多的状态会增加系统中的类的个数,增加系统的复杂性。
iOS SDK 和 JDK中的应用
- javax包下的
LifyCycle
是状态模式的一种实现
五. 命令模式
定义
命令模式(Command Pattern):命令(或请求)被封装成对象。客户端将命令(或请求)对象先传递给调用对象。调用对象再把该命令(或请求)对象传给合适的,可处理该命令(或请求)的对象来做处理。
由定义可以看出,在命令模式中,命令被封装成了对象,而发送命令的客户端与处理命令的接收者中间被调用对象隔开了,这种设计的原因或者适用的场景是什么样的呢?
适用场景
在有些场景下,任务的处理可能不是需要立即执行的:可能需要记录(日至),撤销或重试(网络请求)。那么在这些场景下,如果任务的请求者和执行者是紧耦合状态下的话就可能会将很多其他执行策略的代码和立即执行的代码混合到一起。
这些其他执行策略,我们暂时称之为控制和管理策略,而如果我们如果想控制和管理请求,就需要:
- 把请求抽象出来
- 让另外一个角色来负责控制和管理请求的任务
因此命令模式就是为此场景量身打造的,它通过:
- 把请求封装成对象
- 使用调用者在客户端和请求处理者之间来做一个“拦截”,方便对请求对象做控制和管理。
现在我们清楚了命令模式的适用场景,下面看一下命令模式的成员和类图。
成员与类图
成员
不包括请求的发起者(客户端),命令模式共有四个成员:
- 抽象命令类(Command):命令类负责声明命令的接口。
- 具体命令类(Concrete Command):具体命令类负责实现抽象命令类声明的接口
- 调用者(Invoker):调用者负责将具体命令类的实例传递给接收者
- 接收者(Receiver):接收者负责处理命令
下面通过类图来看一下命令模式各个成员之间的关系:
模式类图
代码示例
场景概述
模拟一个使用遥控器开灯和关灯的例子。
场景分析
在这个例子中,使用遥控器的人就是客户端,TA发起开启或关闭灯的命令给遥控器(调用者)。然后调用者将命令传递给接收者(灯)。
在这里,人是不直接接触灯的,开启和关闭的命令是通过遥控器来做的转发,最后传达给灯来执行。
下面我们看一下如何用代码来模拟该场景。
代码实现
首先我们创建接收者,灯类:
1 | //================== Light.h ================== |
灯类声明并实现了两个接口:开灯接口和关灯接口,来让外部执行开灯和关灯的操作。
接着我们创建抽象命令类和具体命令类:
抽象命令类:
1 | //================== Command.h ================== |
抽象命令类声明了一个执行命令的接口excute
,这个接口由它的子类,也就是具体命令类来实现。
因为这里面只有开灯和关灯两种命令,所以我们创建两个具体命令类来继承上面的抽象命令类:
开灯命令CommandLightOn
:
1 | //================== CommandLightOn.h ================== |
关灯命令CommandLightOff
: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//================== CommandLightOff.h ==================
@interface CommandLightOff : Command
- (instancetype)initWithLight:(Light *)light;
@end
//================== CommandLightOff.m ==================
@implementation CommandLightOff
{
Light *_light;
}
- (instancetype)initWithLight:(Light *)light{
self = [super init];
if (self) {
_light = light;
}
return self;
}
- (void)excute{
[_light lightOff];
}
我们可以看到这两个具体命令类分别以自己的方式实现了它们的父类声明的
excute
接口。
最后我们创建链接客户端和接收者的调用者类,也就是遥控器类RemoteControl
:
1 | //================== RemoteControl.h ================== |
遥控器类使用set
方法注入了具体命令类,并向外提供了pressButton
这个方法来内部调用已传入的具体命令类的excute
方法。
最后我们看一下客户端是如何操作这些类的:
1 | //================== client ================== |
看一下日至输出:
1 | [11851:1190777] Light on |
从上面的代码可以看到,我们首先准备好具体命令类的实例,然后将其传递给遥控器类,最后触发遥控器的pressButton
方法来间接触发light
对象的相应操作。
下面看一下上面代码对应的类图。
代码对应的类图
优点
- 将命令的发起者和命令的执行者分离,降低系统的耦合度
- 便于批量处理命令,比如日至队列的实现;便于命令的撤销或重试,比如网络请求等
缺点
- 需要针对每一个命令创建一个命令对象。如果系统中的命令过多,会造成系统中存在大量的命令类,提高系统的复杂度。
iOS SDK 和 JDK中的应用
- 在JDK中,
java.lang.Runnable
是使用命令模式的经典场景,Runnable接口可以作为抽象的命令,而实现了Runnable的线程即是具体的命令。
六. 观察者模式
定义
观察者模式(Observer Pattern):定义对象间的一种一对多的依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象都可以到通知并做相应针对性的处理。
适用场景
凡是涉及到一对一或者一对多的对象交互场景都可以使用观察者模式。通常我们使用观察者模式实现一个对象的改变会令其他一个或多个对象发生改变的需求,比如换肤功能,监听列表滚动的偏移量等等。
现在我们清楚了观察者模式的适用场景,下面看一下观察者模式的成员和类图。
成员与类图
成员
观察者模式有四个成员:
- 目标(Subject):目标是被观察的角色,声明添加和删除观察者以及通知观察者的接口。
- 具体目标(Concrete Subject):具体目标实现目标类声明的接口,保存所有观察者的实例(通过集合的形式)。在被观察的状态发生变化时,给所有登记过的观察者发送通知。
- 观察者(Observer):观察者定义具体观察者的更新接口,以便在收到通知时实现一些操作。
- 具体观察者(Concrete Observer):具体观察者实现抽象观察者定义的更新接口。
下面通过类图来看一下各个成员之间的关系:
模式类图
代码示例
场景概述
模拟这样的一个场景:客户(投资者)订阅理财顾问的建议购买不同价格的股票。当价格信息变化时,所有客户会收到通知(可以使短信,邮件等等),随后客户查看最新数据并进行操作。
场景分析
一个理财顾问可能服务于多个客户,而且消息需要及时传达到各个客户那边;而客户接收到这些消息后,需要对这些消息做出相应的措施。这种一对多的通知场景我们可以使用观察者模式:理财顾问是被观察的目标(Subject),而TA的客户则是观察者(Observer)。
下面我们看一下如何用代码来模拟该场景。
代码实现
首先我们定义观察者Observer
:
1 | //================== Observer.h ================== |
Observer
类是具体观察者的父类,它声明了一个传入目标类(Subject
)的构造方法并在构造方法里持有这个传入的实例。而且在这个构造方法里,调用了Subject
的‘添加观察者’的方法,即addObserver:
,目的是将当前的观察者实例放入Subject
的用来保存观察者实例的集合中(具体操作可以在下面讲解Subject
类的部分看到)
另外它也定义了update
方法供子类使用。
下面我们看一下具体观察者类Investor
:
1 | //================== Investor.h ================== |
具体观察者实现了该协议中定义的方法update
方法,在这个方法里面,首先通过getBuyingPrice
方法获得到最新的在监听的数据buyingPrice
,然后再做其他操作。这里为了方便展示,直接使用日至打印出当前的具体观察者实例的内存地址和当前监听的最新值。
下面我们声明一下目标类和具体目标类:
目标类Subject
1 | //================== Subject.h ================== |
目标类持有一个可变数组,用来保存观察自己的观察者们;并且还提供了增加,删除观察者的接口,也提供了通知所有观察者的接口。
而且它持有一个数据buyingPrice
,这个数据就是让外部观察者观察的数据。尤其注意它向外界提供的setBuyingPrice:
方法:当外部调用这个方法,也就是要更新buyingPrice
这个数据时,目标类调用了notifyObservers
方法来告知当前所有观察自己的观察者们:我更新了。
而getBuyingPrice
就是用来返回当前的buyingPrice
的值的,一般是在观察者们收到更新通知后,主动调动这个方法获取的(具体看上面Investor
类的实现)。
OK,现在抽象目标类定义好了,下面我们看一下具体目标类FinancialAdviser
:
1 | //================== FinancialAdviser.h ================== |
因为所有的接口的事先已经在Subject
类定义好了,所以我们只需新建一个我们需要的子类即可(如果有不同于父类的操作的话还是可以按照自己的方式定义)。
下面我们看一下观察者的机制是如何实现的:
1 | FinancialAdviser *fa = [[FinancialAdviser alloc] init]; |
从代码中可以看到,我们最开始向FinancialAdviser
(具体目标类)添加了一个具体观察者类的实例iv1
,然后FinancialAdviser
的实例fa
便通知了所有观察者(此时的观察者只有iv1
)。
后面我们继续向fa
添加了iv2
和iv3
后发送通知。此时三个观察者都收到了消息。
在下面的日至输出中也可以看到,内存地址0x600003094c00
就是iv1
,0x600003083680
和0x600003083690
就是iv2
和iv3
。
1 | ====== first advice ======== |
下面看一下上面代码对应的类图。
代码对应的类图
优点
- 观察者模式在观察目标和观察者之间建立了一个抽象的耦合。
- 可实现广播的,一对多的通信
缺点
- 如果一个观察目标对象有很多直接和间接的观察者的话,会需要比较多的通信时间。
- 需要注意观察者和观察目标之间是否有循环引用。
iOS SDK 和 JDK中的应用
- 在 iOS SDK 中的 KVO 与 NSNotification 是观察者模式的应用。
- 在JDK的
java.util
包中,提供了Observable
类以及Observer
接口,它们构成了Java语言对观察者模式的支持。
七. 中介者模式
定义
中介者模式(Mediator Pattern):用一个中介对象来封装一系列的对象交互,中介者使各对象之间不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
适用场景
系统结构可能会日益变得复杂,对象之间存在大量的相互关联和调用,系统的整体结构容易变为网状结构。在这种情况下,如果需要修改某一个对象,则可能会要跟踪和该对象关联的其他所有对象,并进行处理。耦合越多,修改的地方就会越多。
如果我们使用中介者对象,则可以将系统的网状结构变成以中介者为中心的星型结构。中介者承担了中转作用和协调作用,简化了对象之间的交互,而且还可以给对象间的交互进行进一步的控制。
现在我们清楚了中介者模式的适用场景,下面看一下中介者模式的成员和类图。
成员与类图
成员
中介者模式一共有四个成员:
- 抽象中介者(Mediator):抽象中介者定义具体中介者需要实现的接口。
- 具体中介者(Concrete Mediator):具体中介者实现抽象中介者定义的接口,承担多个具体同事类之间的中介者的角色。
- 抽象同事类(Colleague):抽象同事类定义具体同事类需要实现的接口。
- 具体同事类(Concrete Colleague):具体同事类实现抽象同事类定义的接口。
模式类图
代码示例
场景概述
模拟一个多人对话的场景:当一个人发出消息后,另外的那些人可以收到该消息。
场景分析
假设一共有A,B,C三个人,那么当A发出消息后,需要分别传递给B,C二人。如果三个人直接相互通信,可能伪代码会是这样的:
1 | A sent message to B |
而且随着人数的增多,代码行数也会变多,这显然是不合理的。
因此在这种场景下,我们需要使用中介者模式,在所有人中间来做一个消息的多路转发:当A发出消息后,由中介者来发送给B和C:
1 | A sent message to Mediator ; |
下面我们看一下如何用代码来模拟该场景。
代码实现
首先我们创建通话的用户类User
:
1 | //================== User.h ================== |
用户类在初始化的时候需要传入中介者的实例,并持有。目的是为了在后面发送消息的时候把消息转发给中介者。
另外,用户类还对外提供了发送消息和接收消息的接口。而在发送消息的方法内部其实调用的是中介者的发送消息的方法(因为中介者持有了所有用户的实例,因此可以做多路转发),具体是如何做的我们可以看下中介者类ChatMediator
的实现:
1 | //================== ChatMediator.h ================== |
中介者类提供了addUser:
的方法,因此我们可以不断将用户添加到这个中介者里面(可以看做是注册行为或是“加入群聊”)。在每次加入一个User
实例后,都将这个实例添加到中介者持有的这个可变数组里。于是在将来中介者就可以通过遍历数组的方式来做消息的多路转发,具体实现可以看sendMessage:fromUser:
这个方法。
到现在为止,用户类和中介者类都创建好了,我们看一下消息是如何转发的:
1 | ChatMediator *cm = [[ChatMediator alloc] init]; |
从代码中可以看到,我们这里创建了三个用户,分别加入到了聊天中介者对象里。再后面我们分别让每个用户发送了一条消息。我们下面通过日至输出来看一下每个用户的消息接收情况:
1 | [13806:1284059] ================ |
下面看一下上面代码对应的类图。
代码对应的类图
优点
- 中介者使各对象不需要显式地相互引用,从而使其耦合松散。
缺点
- 在具体中介者类中包含了同事类之间的交互细节,可能会导致具体中介者类非常复杂,使得其难以维护。
iOS SDK 和 JDK中的应用
- JDK中的
Timer
就是中介者类的实现,而配合使用的TimerTask
则是同事类的实现。
到这里设计模式中的行为型模式就介绍完了,读者可以结合UML类图和demo的代码来理解每个设计模式的特点和相互之间的区别,希望读者可以有所收获。
本篇博客的代码和类图都保存在我的GitHub库中:knightsj:object-oriented-design中的 Chapter 2.3。
到本篇为止,面向对象设计系列暂时告一段落,短期内不会有新的文章出来。读者朋友们可以随时给我提意见或沟通。
该系列前面的三篇文章:
- 面向对象设计的六大设计原则(附 Demo 及 UML 类图)
- 面向对象设计的设计模式(一):创建型设计模式(附 Demo 及 UML 类图)
- 面向对象设计的设计模式(二):结构型模式(附 Demo & UML类图)