行为模式
这次总结行为模式。行为模式主要涉及算法涉及对象的职责分配问题。
迭代器模式
先介绍这个模式是因为它太常见了。各种编程语言的容器都有自己的迭代器。比如:Java中的List是个接口。只要符合List接口的各种List都可以使用它的迭代器来按照某种顺序遍历访问List的元素,而不必关心具体List的实现(是ArrayList还是LinkedList之类的)。
迭代器模式的主要用来:
访问一个聚合对象的内容而不必暴露它的内部表示。
支持聚合对象的各种遍历。
为遍历不同的聚合结构提供一个统一的接口(多态迭代)。
参与者
Iterator
--迭代器定义访问和遍历元素的接口。在Java中List的iterator()方法返回一个实现了Iterator接口的迭代器。这是典型的Iterator。
ConcreteIterator
--具体的迭代器实现了Iterator接口。并能在遍历容器时跟踪当前位置。
Aggregate
--定义创建响应迭代器对象的接口。也就是它可以返回Iterator。比如:Java的List。
ConcreteAggregate
--实现了创建响应迭代器对象的接口,返回迭代器实例。
推荐看Java中LinkedList的代码。
职责链模式
这个模式用来避免请求的发送者和接受者之间的耦合关系。如果有多个对象,它们都有机会处理请求(但是何时处理的条件不同),就可以把对象连成一条链,沿着该链传递请求,知道有一个对象处理它为止。
举例:我们看龙珠或者其它战斗类型的漫画的时候会发现一个问题:主角去挑战大Boss的时候总是先打小Boss,再打大Boss,也就是说小Boss挂了或搞不定的时候大 Boss才会亲自动手。这就形成了一个责任链!可以把主角的挑战看成是请求,职责链上的对象(各种Boss)负责处理请求。如果职责链上的对象发现自己无法处理请求(小Boss打不过挂掉了),这个请求就会转发到下一对象(更高级的Boss),否则就处理这个请求(就是Boss赢了)。可以把打的过程看成是判断是否可以处理请求,输赢看成转发和处理。
这里需要注意,职责链上的对象在转发请求的时候是不知道最终会由谁处理请求的(小Boss打输后不是很确定后面的Boss会不会赢)。
上面这个例子的参与者
Handler(Boss 类)
--定义一个处理请求的接口。在这里可以抽象出一个Boss类。 --(可选)实现后继链。
ConcreteHandler(Boss类的子类,各种Boss)
--处理它负责的请求。
--可以访问后继者。
--如果能处理请求就处理,否则转发请求给后继者。
Client(主角)
--向链上的具体处理者ConcreteHandler提交请求。
例子的代码: 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
using namespace std;
class Kakarotto;
class Boss{//handler
Boss *successor;
protected:
int power;
public:
Boss(int p, Boss *s):power(p), successor(s){}
virtual void handleChallenge(Kakarotto *k){
if(successor==NULL){
cout<<"Challenger win!"<<endl;
return;
}
successor->handleChallenge(k);
}
};
class Kakarotto{//client
int power;
public:
Kakarotto(int p):power(p){}
void Challenge(Boss *b){
cout<<"Challenging"<<endl;
b->handleChallenge(this);
}
int getPower(){return power;}
};
class Vegeta: public Boss{//concrete handler 1
public:
Vegeta(int p, Boss *s):Boss(p, s){
}
virtual void handleChallenge(Kakarotto* k){
cout<<"Vegeta is challenged!"<<endl;
if(k->getPower()<power){
cout<<"Vegeta win!"<<endl;
}else{
cout<<"Vegeta failed"<<endl;
Boss::handleChallenge(k);
}
}
};
class Freeze: public Boss{//concrete handler 2
public:
Freeze(int p, Boss *s):Boss(p, s){}
virtual void handleChallenge(Kakarotto* k){
cout<<"Freeze is challenged!"<<endl;
if(k->getPower()<power){
cout<<"Freeze win!"<<endl;
}else{
cout<<"Freeze failed"<<endl;
Boss::handleChallenge(k);
}
}
};
int main(){
Kakarotto * k = new Kakarotto(10000);
Freeze *f = new Freeze(1000000, NULL);
Vegeta *v = new Vegeta(5000, f);
k->Challenge(v);
return 0;
}
命令模式
将请求封装为对象,可用不同的请求对客户进行参数化。用于“行为请求者”与“行为实现者”解耦。
举例:
现在有一个文档处理程序中用到了菜单。菜单中有许多菜单项。每个菜单项执行一个或一系列命令。程序用某种UI框架开发。显然,框架开发者是不知道菜单具体会执行哪些功能的,它只知道何时执行(即用户触发了Click事件它可以捕捉到,然后去执行具体功能)。具体的命令,将被封装成对象,以供调用。
在这个例子中,要执行的命令都实现了Command接口中的方法。菜单项MenuItem就是负则调用Command接口中方法的类,从而在用户点击菜单项时执行操作(准确说操作是Command类的子类里的接受者Receiver执行的)。应用程序类为App,它持有具体的命令对象(Command的子类)并绑定了接受者,建立了MenuItem具体Item类的联系。Receiver是文档类Document,它知道如何如何对文档实施具体的操作。
上面这个例子的参与者
Command
--声明操作的接口
ConcreteCommand(复制、粘贴等等)
--将一个接受者对象绑定于一个动作。
--调用接受者相应的操作,从而实现execute()。
Client(App)
--创建具体命令对象并设置接受者。
Invoker(菜单项MenuItem)
--要求该命令执行这个请求。注意一个Invoker可以对应多个Command。
Receiver(Document)
--知道如何实施与执行一个请求相关的操作。
这里有个疑问:为什么需要Command类,而不是直接使用Invoker?这是因为Command类的execute()方法中可以做点别的工作,比如记录指令日志,从而支持撤销命令等等。
命令模式和过程式语言的回调函数机制十分类似。
下面是一个例子代码: 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
81package com.vbill;
class Doc{//receiver
String name;
public Doc(String name){
this.name = name;
}
public void edit(){
System.out.println("edit "+name);
}
public void open(){
System.out.println("open "+name);
}
}
abstract class Command{//command
protected Doc doc;
public Command(Doc doc){ this.doc = doc; }
abstract public void execute();
}
class EditCommmand extends Command{//concrete command
public EditCommmand(Doc doc){
super(doc);
}
public void execute(){
doc.edit();
}
}
class OpenCommmand extends Command{//concrete command
public OpenCommmand(Doc doc){
super(doc);
}
public void execute(){
doc.open();
}
}
class Invoker extends Thread{//invoker
private Command com;
public void run() {
System.out.println("waiting for invoke!");
try{
sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
com.execute();
}
public void setCom(Command com){ this.com = com; }
}
public class Main {
public static void main(String[] args) {//client
Command coms[] = new Command[]{ new OpenCommmand(new Doc("1.txt")),
new EditCommmand(new Doc("1.txt"))};
Invoker inv = new Invoker();
for(Command com : coms) {
inv.setCom(com);
inv.run();
try {
inv.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
策略模式
定义了一系列的算法的封装,使得它们可以相互替换。这使得算法可以独立于使用它的客户而变化。
显然,因为算法之间是可以替换的,所以它们有相同的接口,我们管这个接口叫做策略(Strategy)。
举个例子:现在有一个程序提供了一个搜索条,根据文本的长短和各个字符出现的频率,它可以调整自己的字符串搜索算法。
以上例子中的参与者
Strategy
--定义了所有算法的公共接口。Context使用这个接口来调用ConcreteStrategy定义的算法。上面这个例子中,可以定义一个只含1个方法的接口,它接收2个参数,一个是正文的指针,另一个是待搜索串的指针。
ConcreteStrategy
--以Strategy接口实现的算法。在这个例子中就是各种字符串搜索算法。
Context
--上下文。用1个ConcreteStrategy对象来配置。维护一个ConcreteStrategy对象的引用。并且可定义一个接口让Strategy访问它的数据。
下面是一个配置了不同的字符串搜索算法的策略模式的例子: 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
71package com.vbill;
interface StrfindStrategy{
int strFind(String dest, String pattern);
}
class KMPFinder implements StrfindStrategy{
int next[];
private void calcNext(String pattern){
next = new int[pattern.length()];
next[0] = -1;
for(int i=1, j = -1; i<pattern.length(); i++){
while (j>=0 && pattern.charAt(j+1) != pattern.charAt(i))
j = next[j];
if(pattern.charAt(j+1) == pattern.charAt(i)) j++;
next[i] = j;
}
}
public int strFind(String dest, String pattern){
if(pattern.length()>dest.length())return -1;
calcNext(pattern);
for(int i=0, j=-1; i<dest.length(); i++){
while(j>=0 && pattern.charAt(j+1)!=dest.charAt(i))
j = next[j];
if(pattern.charAt(j+1)==dest.charAt(i)) j++;
if(j==pattern.length()-1)
return i-pattern.length()+1;
}
return -1;
}
}
class DefaultFinder implements StrfindStrategy{
public int strFind(String dest, String pattern){
return dest.indexOf(pattern);
}
}
class Context{
private StrfindStrategy strategy;
public Context(StrfindStrategy s){
this.strategy = s;
}
public int execute(String dest, String pattern){
return strategy.strFind(dest, pattern);
}
public void setStrategy(StrfindStrategy s){
this.strategy = s;
}
}
public class Main {
public static void main(String[] args) {//client
String dest = "xbaadabcabab";
String pattern = "abab";
Context con = new Context(new DefaultFinder());
System.out.println(con.execute(dest, pattern));
con.setStrategy(new KMPFinder());
System.out.println(con.execute(dest, pattern));
}
}
观察者模式
定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
举个例子:现在有许多个统计图(柱状图、折现图、扇形图),这些图都反映了同一个表格的数据,如果表格中的数据改变了,图中的内容也要自动发生响应的变化。
模式的参与者
Subject
--目标知道它的观察者,可以有多个观察者观察同一个目标。
--提供注册和删除观察者的接口。
Observer
--为那些在目标发生改变时需要获得通知的对象定义一个更新接口。
ConcreteSubject
--将有关状态存入各ConcreteObserver对象。
--当它的状态发生改变时,向它的各个观察者发出通知。
ConcreteObserver
--维护一个指向ConcreteSubject对象的引用
--存储有关状态,这些状态应与目标的状态保持一致。
--实现Observer的更新接口以使自身状态与目标的状态保持一致。
在Java语言中自带了该模式相关的接口,下面是例子:
1 | package com.vbill; |
在jdk源代码里,Observable接口(相当于Subject)是这样实现的:
1 | package java.util; |
jdk中Observer接口的源代码:
1 | public interface Observer { |
备忘录模式
在不破坏封装性的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态,这样就可以将该对象恢复到原先保存的状态。
在这个模式中一个备忘录就是一个用来记录状态的对象,它记录的是某个对象在某个时刻的状态。被记录的对象称为Originator。当需要进行记录的时候,Originator请求一个备忘录,并用自己的当前状态设置备忘录对象。只有Originator能够存取备忘录的信息,其它对象则不可以。
参与该模式的参与者
Memento
--备忘录存储原发器对象的内部状态。原发器根据需要决定存储哪些内部状态。
--防止原发器外的其它对象访问备忘录。备忘录实际有2个接口,一个是Caretaker看到的窄接口,它只能将备忘录传递给其它对象。原发器只能看到宽接口,允许它访问返回到先前状态所需要的数据。理想的情况是只允许生成本备忘录的那个原发器访问本备忘录的状态。
Originator
--原发器创建一个备忘录记录当前自己的状态。
--使用备忘录恢复内部状态。
Caretaker
--负责保存备忘录
--不能对备忘录的内容进行检查操作或检查。
下面是一份Java代码的例子:
1 | package com.vbill; |
状态模式
允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
也就是说这个对象的行为随着状态改变!
举个例子:现在有个2状态状态机,状态分别叫0和1。状态机只有2个输入0和1。在0状态下输入0状态始终为0,在0状态下输入1会切换到1。在1状态下输入1状态始终为1,输入0则切换到0状态。并且你有1个LED灯,当状态为1时灯亮,否则灯灭。
参与者
Context
--定义用户感兴趣的接口。
State
--定义一个接口以封装与Context的一个特定状态相关的行为。
ConcreteState
--每一个子类实现一个与Context的一个状态相关的行为。
需要注意的是这个模式和策略模式非常相似。如果画出UML类图,会发现两者几乎是一样的。那么问题来了,究竟它们有啥区别?
在状态模式中,Context或ConcreteState子类都可以决定哪个状态是另外一个的后继者,以及是在何种条件下进行状态转换。
在策略模式中,Context将它的客户的请求转发给它的Strategy。客户通常创建并传递一个ConcreteStrategy对象给该Context;这样客户仅与Context交互。通常有一系列的ConcreteStrategy类可供客户从中选择。
上面的状态机的例子的Java代码:
1 | package com.vbill; |
中介者模式
用一个中介对象来封装一些列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其松散耦合,而且可以独立地改变它们之间的交互。
举例:现在有个对话框,上面有很多部件。比如:有文本框、列表框、一个确定按钮,仅当文本框里有内容时,按钮才能按下,按下按钮后文本框的内容将会被发送给程序的后台处理。在用户懒得输入文本框内容时,可以从列表框里选择几个预置的内容填入文本框。
如果直接让各个对象之间进行通信,耦合度是很高的。如果把各种集体行为封装在一个中介者(mediator)中就可以避免这个问题。中介者用来协调一组对象的交互,它使得对象之间不再显式的相互调用,这些对象只知道中介者。中介者就是对象通信的中转中心。
参与者
Mediator
--中介者定义一个接口用于与各个同事对象通信。
ConcreteMediator
--具体中介者通过协调各个同事对象实现协作行为。
--了解并维护它的各个同事。
Colleague Class
--每个同事类都知道它的中介者对象。
--每一个同事对象在需要与其它同事通信的时候都与它的中介者通信。
上面提到的例子的Java代码如下(不含测试类)
1 | import java.util.*; |
访问者模式
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
举例:这里有一个完整的例子。
模式的参与者
Visitor
--为该对象结构中的ConcreteElement的每一个类声明一个visit操作。该操作的名字和特征标示了发送visit请求给该访问者的那个类。这使得访问者可以确定被访问元素的具体的类。这样访问者就可以通过该元素的特定接口直接访问它。
ConcreteVisitor
--实现每个由Visitor声明的操作。每个操作实现本算法的一部分,而该算法片段乃是对应于结构中对象的类。ConcreteVisitor为该算法提供了上下文并存储它的局部状态。这一状态常常在遍历该结构的过程中基类结果。
Element
--定义一个Accept操作,它以一个访问者为参数。
ConcreteElement
--实现Accept操作,该操作以一个访问者为参数。
ObjectStructure
--能枚举它的元素。
--可以提供一个高层的接口以允许该访问者访问它的元素。‘
--可以是一个符合(Composite)或是一个集合,如一个列表或一个无序集合。
解释器模式
给定一个语言,定义它的文法表示,并定义一个解释器,将这个解释器使用该表示来解释语言中的句子。
这个模式应用的情景比较特殊(个人感觉),比如:HTML解释器,正则表达式等等,甚至解释型语言的解释器。
模式参与者
AbstractExpression
--声明一个抽象的解释操作,这个接口为抽象语法树中所有节点所共享。
TerminalExpression
--实现与文法中的终结符相关的解释操作。
--一个句子中的每个终结符需要该类的一个实例。
NonterminalExpression
--对文法中的每一条文法规则
R::R1R2...Rn
都需要一个NonterminalExpression类。--为从R1到到Rn的每个符号都维护一个AbstractExpression类型的实例变量。
--为文法中的非终结符实现解释操作。解释一般要递归地调用表示R1到Rn的那些对象的解释操作。
Context
--包含解释器之外的一些全局信息。
Client
--构建(或被给定)表示该文法定义的语言中一个特定的句子的抽象语法树。该抽象语法树由NonterminalExpression和TerminalExpreession的实例装配而成。
--调用解释操作。