0%

在软件开发系统中,常常出现“方法的请求者”与“方法的实现者”之间存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对行为进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与方法的实现者解耦?”变得很重要,命令模式能很好地解决这个问题。

命令(Command)模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。

命令模式的主要优点如下:

  • 降低系统的耦合度:命令模式能将调用操作的对象与实现该操作的对象解耦。
  • 增加或删除命令非常方便:采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
  • 可以实现宏命令:命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  • 方便实现 Undo 和 Redo 操作:命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。

其缺点是:可能产生大量具体命令类。因为计对每一个具体操作都需要设计一个具体命令类,这将增加系统的复杂性。

可以将系统中的相关操作抽象成命令,使调用者与实现者相关分离,其结构如下。

模式的结构#

命令模式包含以下主要角色:

  • 抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute()。
  • 具体命令角色(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作
  • 实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
  • 调用者/请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。

其结构图如图所示:

模式的实现#

命令模式的代码如下:

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
class Program
{
static void Main(string[] args)
{
ICommand cmd=new ConcreteCommand();
Invoker ir=new Invoker(cmd);
Console.WriteLine("客户访问调用者的Call()方法...");
ir.Call();
Console.Read();

}
}


public class Invoker
{
private ICommand command;
public Invoker(ICommand command)
{
this.command=command;
}
public void SetCommand(ICommand command)
{
this.command=command;
}
public void Call()
{
Console.WriteLine("调用者执行命令command...");
command.Execute();
}
}


public interface ICommand
{
void Execute();
}


public class ConcreteCommand : ICommand
{
private Receiver receiver;
public ConcreteCommand()
{
receiver=new Receiver();
}
public void Execute()
{
receiver.Action();
}
}


public class Receiver
{
public void Action()
{
Console.WriteLine("接收者的Action()方法被调用...");
}
}

程序的运行结果如下:

1
2
3
客户访问调用者的Call()方法...
调用者执行命令command...
接收者的Action()方法被调用...

命令模式通常适用于以下场景:

  • 当系统需要将请求调用者与请求接收者解耦时,命令模式使得调用者和接收者不直接交互。
  • 当系统需要随机请求命令或经常增加或删除命令时,命令模式比较方便实现这些功能。
  • 当系统需要执行一组操作时,命令模式可以定义宏命令来实现该功能。
  • 当系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作时,可以将命令对象存储起来,采用备忘录模式来实现。

在软件开发中,有时将命令模式与前面学的组合模式联合使用,这就构成了宏命令模式,也叫组合命令模式。宏命令包含了一组命令,它充当了具体命令与调用者的双重角色,执行它时将递归调用它所包含的所有命令,其具体结构图如图所示:

程序代码如下:

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
86
87
class Program
{
static void Main(string[] args)
{
IAbstractCommand cmd1=new ConcreteCommand1();
IAbstractCommand cmd2=new ConcreteCommand2();
CompositeInvoker ir=new CompositeInvoker();
ir.Add(cmd1);
ir.Add(cmd2);
Console.WriteLine("客户访问调用者的Execute()方法...");
ir.Execute();
Console.Read();

}
}


public interface IAbstractCommand
{
void Execute();
}


public class ConcreteCommand1 : IAbstractCommand
{
private CompositeReceiver receiver;
public ConcreteCommand1()
{
receiver=new CompositeReceiver();
}
public void Execute()
{
receiver.Action1();
}
}


public class ConcreteCommand2 : IAbstractCommand
{
private CompositeReceiver receiver;
public ConcreteCommand2()
{
receiver=new CompositeReceiver();
}
public void Execute()
{
receiver.Action2();
}
}


public class CompositeInvoker : IAbstractCommand
{
private List<IAbstractCommand> children = new List<IAbstractCommand>();
public void Add(IAbstractCommand c)
{
children.Add(c);
}
public void Remove(IAbstractCommand c)
{
children.Remove(c);
}
public IAbstractCommand GetChild(int i)
{
return children[i];
}
public void Execute()
{
foreach (var child in children)
{
child.Execute();
}
}
}


public class CompositeReceiver
{
public void Action1()
{
Console.WriteLine("接收者的Action1()方法被调用...");
}
public void Action2()
{
Console.WriteLine("接收者的Action2()方法被调用...");
}
}

程序的运行结果如下:

1
2
3
客户访问调用者的Execute()方法...
接收者的Action1()方法被调用...
接收者的Action2()方法被调用...

当然,命令模式还可以同备忘录(Memento)模式组合使用,这样就变成了可撤销的命令模式,这将在后面介绍。

在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。
例如,一个人每天会起床、吃饭、做事、睡觉等,其中“做事”的内容每天可能不同。我们把这些规定了流程或格式的实例定义成模板,允许使用者根据自己的需求去更新它,例如,简历模板、论文模板、Word 中模板文件等。

模板方法(Template Method)模式的定义如下:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。

该模式的主要优点如下:

  • 封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
  • 它在父类中提取了公共的部分代码,便于代码复用
  • 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则

该模式的主要缺点如下:

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度

模板方法模式需要注意抽象类与具体子类之间的协作。它用到了虚函数的多态性技术以及“不用调用我,让我来调用你”的反向控制技术

模式的结构#

模板方法模式包含以下主要角色:

抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成,这些方法的定义如下:

  • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
  • 基本方法:是整个算法中的一个步骤,包含以下几种类型:
  • 抽象方法:在抽象类中申明,由具体子类实现
  • 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它
  • 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种

具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤。

模板方法模式的结构图如图所示:

模式的实现#

模板方法模式的代码如下:

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
class Program
{
static void Main(string[] args)
{
AbstractClass tm=new ConcreteClass();
tm.TemplateMethod();

Console.Read();
}
}


public abstract class AbstractClass
{
public void TemplateMethod()
{
SpecificMethod();
AbstractMethod1();
AbstractMethod2();
}
public void SpecificMethod()
{
Console.WriteLine("抽象类中的具体方法被调用...");
}
public abstract void AbstractMethod1();
public abstract void AbstractMethod2();
}


public class ConcreteClass : AbstractClass
{
public override void AbstractMethod1()
{
Console.WriteLine("抽象方法1的实现被调用...");
}
public override void AbstractMethod2()
{
Console.WriteLine("抽象方法2的实现被调用...");
}
}

程序的运行结果如下:

1
2
3
抽象类中的具体方法被调用...
抽象方法1的实现被调用...
抽象方法2的实现被调用...

模板方法模式通常适用于以下场景:

  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  • 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
  • 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展

在模板方法模式中,基本方法包含:抽象方法、具体方法和钩子方法,正确使用“钩子方法”可以使得子类控制父类的行为。如下面例子中,可以通过在具体子类中重写钩子方法 HookMethod1() 和 HookMethod2() 来改变抽象父类中的运行结果,其结构图如图所示:

程序代码如下:

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
class Program
{
static void Main(string[] args)
{
HookAbstractClass tm = new HookConcreteClass();
tm.TemplateMethod();

Console.Read();
}
}


public abstract class HookAbstractClass
{
public void TemplateMethod()
{
AbstractMethod1();
HookMethod1();
if(HookMethod2())
{
SpecificMethod();
}
AbstractMethod2();
}
public void SpecificMethod()
{
Console.WriteLine("抽象类中的具体方法被调用...");
}
public virtual void HookMethod1(){}
public virtual bool HookMethod2()
{
return true;
}
public abstract void AbstractMethod1();
public abstract void AbstractMethod2();
}


public class HookConcreteClass : HookAbstractClass
{
public override void AbstractMethod1()
{
Console.WriteLine("抽象方法1的实现被调用...");
}
public override void AbstractMethod2()
{
Console.WriteLine("抽象方法2的实现被调用...");
}
public override void HookMethod1()
{
Console.WriteLine("钩子方法1被重写...");
}
public override bool HookMethod2()
{
return false;
}
}

在软件开发过程中,应用程序中的有些对象可能会根据不同的情况做出不同的行为,我们把这种对象称为有状态的对象,而把影响对象行为的一个或多个动态变化的属性称为状态。当有状态的对象与外部事件产生互动时,其内部状态会发生改变,从而使得其行为也随之发生改变。

对这种有状态的对象编程,传统的解决方案是:将这些所有可能发生的情况全都考虑到,然后使用 if-else 语句来做状态判断,再进行不同情况的处理。但当对象的状态很多时,程序会变得很复杂。而且增加新的状态要添加新的 if-else 语句,这违背了“开闭原则”,不利于程序的扩展。

以上问题如果采用“状态模式”就能很好地得到解决。状态模式的解决思想是:当控制一个对象状态转换的条件表达式过于复杂时,把相关“判断逻辑”提取出来,放到一系列的状态类当中,这样可以把原来复杂的逻辑判断简单化

状态(State)模式的定义:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。

状态模式是一种对象行为型模式,其主要优点如下:

  • 状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”
  • 减少对象间的相互依赖,将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
  • 有利于程序的扩展,通过定义新的子类很容易地增加新的状态和转换。

状态模式的主要缺点如下:

  • 状态模式的使用必然会增加系统的类与对象的个数
  • 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。

状态模式把受环境改变的对象行为包装在不同的状态对象里,其意图是让一个对象在其内部状态改变的时候,其行为也随之改变。

模式的结构#

状态模式包含以下主要角色:

  • 环境(Context)角色:也称为上下文,它定义了客户感兴趣的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理
  • 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为
  • 具体状态(Concrete State)角色:实现抽象状态所对应的行为

其结构图如图所示:

模式的实现#

状态模式的实现代码如下:

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
class Program
{
static void Main(string[] args)
{
Context context=new Context();
context.Handle();
context.Handle();
context.Handle();
context.Handle();
Console.Read();
}
}


public class Context
{
private State state;

public Context()
{
this.state=new ConcreteStateA();
}

public void SetState(State state)
{
this.state=state;
}

public State GetState()
{
return(state);
}

public void Handle()
{
state.Handle(this);
}
}


public abstract class State
{
public abstract void Handle(Context context);
}


public class ConcreteStateA : State
{
public override void Handle(Context context)
{
Console.WriteLine("当前状态是 A.");
context.SetState(new ConcreteStateB());
}
}


public class ConcreteStateB : State
{
public override void Handle(Context context)
{
Console.WriteLine("当前状态是 B.");
context.SetState(new ConcreteStateA());
}
}

程序运行结果如下:

1
2
3
4
当前状态是 A.
当前状态是 B.
当前状态是 A.
当前状态是 B.

通常在以下情况下可以考虑使用状态模式:

  • 一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
  • 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时

在有些情况下,可能有多个环境对象需要共享一组状态,这时需要引入享元模式,将这些具体状态对象放在集合中供程序共享,其结构图如图所示:

分析:共享状态模式的不同之处是在环境类中增加了一个 HashMap 来保存相关状态,当需要某种状态时可以从中获取,其程序代码如下:

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
class Program
{
static void Main(string[] args)
{
ShareContext context=new ShareContext();
context.Handle();
context.Handle();
context.Handle();
context.Handle();
Console.Read();
}
}


public class ShareContext
{
private ShareState state;
private Dictionary<String, ShareState> stateDic=new Dictionary<String, ShareState>();
public ShareContext()
{
state=new ConcreteState1();
stateDic.Add("1", state);
state=new ConcreteState2();
stateDic.Add("2", state);
state=GetState("1");
}

public void SetState(ShareState state)
{
this.state=state;
}

public ShareState GetState(String key)
{
ShareState s=stateDic[key];
return s;
}

public void Handle()
{
state.Handle(this);
}
}


public abstract class ShareState
{
public abstract void Handle(ShareContext context);
}


public class ConcreteState1 : ShareState
{
public override void Handle(ShareContext context)
{
Console.WriteLine("当前状态是: 状态1");
context.SetState(context.GetState("2"));
}
}


public class ConcreteState2 : ShareState
{
public override void Handle(ShareContext context)
{
Console.WriteLine("当前状态是: 状态2");
context.SetState(context.GetState("1"));
}
}

每个人都有犯错误的时候,都希望有种“后悔药”能弥补自己的过失,让自己重新开始,但现实是残酷的。在计算机应用中,客户同样会常常犯错误,能否提供“后悔药”给他们呢?当然是可以的,而且是有必要的。这个功能由“备忘录模式”来实现。

其实很多应用软件都提供了这项功能,如 Word、记事本、Photoshop、Eclipse 等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 IE 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。

备忘录(Memento)模式的定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态,该模式又叫快照模式

备忘录模式是一种对象行为型模式,其主要优点如下:

  • 提供了一种可以恢复状态的机制:当用户需要时能够比较方便地将数据恢复到某个历史的状态。
  • 实现了内部状态的封装:除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
  • 简化了发起人类:发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。

其主要缺点是:资源消耗大,如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

备忘录模式的核心是设计备忘录类以及用于管理备忘录的管理者类,现在我们来学习其结构与实现。

模式的结构#

备忘录模式的主要角色如下:

  • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。

备忘录模式的结构图如图所示:

模式的实现#

备忘录模式的实现代码如下:

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
class Program
{
static void Main(string[] args)
{
Originator or = new Originator();
Caretaker cr = new Caretaker();
or.SetState("S0");
Console.WriteLine("初始状态:" + or.GetState());
cr.SetMemento(or.CreateMemento());
or.SetState("S1");
Console.WriteLine("新的状态:" + or.GetState());
or.RestoreMemento(cr.GetMemento());
Console.WriteLine("恢复状态:" + or.GetState());
Console.Read();
}
}


public class Memento
{
private String state;
public Memento(String state)
{
this.state=state;
}
public void SetState(String state)
{
this.state=state;
}
public String GetState()
{
return state;
}
}


public class Originator
{
private String state;
public void SetState(String state)
{
this.state=state;
}
public String GetState()
{
return state;
}
public Memento CreateMemento()
{
return new Memento(state);
}
public void RestoreMemento(Memento memento)
{
this.SetState(memento.GetState());
}
}


public class Caretaker
{
private Memento memento;
public void SetMemento(Memento memento)
{
this.memento=memento;
}
public Memento GetMemento()
{
return memento;
}
}

程序运行的结果如下:

1
2
3
初始状态:S0
新的状态:S1
恢复状态:S0

前面学习了备忘录模式的定义与特点、结构与实现,现在来看该模式的以下应用场景:

  • 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
  • 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,Eclipse 等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。

在备忘录模式中,通过定义“备忘录”来备份“发起人”的信息,而原型模式的 clone() 方法具有自备份功能。所以,如果让发起人实现 Cloneable 接口就有备份自己的功能,这时可以删除备忘录类,其结构图如图所示:

实现代码如下:

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
class Program
{
static void Main(string[] args)
{
OriginatorPrototype or=new OriginatorPrototype();
PrototypeCaretaker cr=new PrototypeCaretaker();
or.SetState("S0");
Console.WriteLine("初始状态:"+or.GetState());
cr.SetMemento(or.CreateMemento());
or.SetState("S1");
Console.WriteLine("新的状态:"+or.GetState());
or.RestoreMemento(cr.GetMemento());
Console.WriteLine("恢复状态:"+or.GetState());
Console.Read();
}
}


class OriginatorPrototype : ICloneable
{
private String state;
public void SetState(String state)
{
this.state=state;
}
public String GetState()
{
return state;
}
public OriginatorPrototype CreateMemento()
{
return (OriginatorPrototype)this.Clone();
}
public void RestoreMemento(OriginatorPrototype opt)
{
this.SetState(opt.GetState());
}
public object Clone()
{
try
{
return base.MemberwiseClone();
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
}

class PrototypeCaretaker
{
private OriginatorPrototype opt;
public void SetMemento(OriginatorPrototype opt)
{
this.opt=opt;
}
public OriginatorPrototype GetMemento()
{
return opt;
}
}

程序的运行结果如下:

1
2
3
初始状态:S0
新的状态:S1
恢复状态:S0

在软件开发中常常遇到实现某种目标存在多种策略可供选择的情况,当实现某一个功能存在多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能,如数据排序策略有冒泡排序、选择排序、插入排序、二叉树排序等。如果使用多重条件转移语句实现(即硬编码),不但使条件语句变得很复杂,而且增加、删除或更换算法要修改原代码,不易维护,违背开闭原则。如果采用策略模式就能很好解决该问题。

策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

策略模式的主要优点如下:

  • 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句
  • 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码
  • 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
  • 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
  • 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离

其主要缺点如下:

  • 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
  • 策略模式造成很多的策略类

策略模式是准备一组算法,并将这组算法封装到一系列的策略类里面,作为一个抽象策略类的子类。策略模式的重心不是如何实现算法,而是如何组织这些算法,从而让程序结构更加灵活,具有更好的维护性和扩展性

模式的结构#

策略模式的主要角色如下:

  • 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现
  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现
  • 环境(Context)类:持有一个策略类的引用,最终给客户端调用

其结构图如图所示:

模式的实现#

策略模式的实现代码如下:

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
class Program
{
static void Main(string[] args)
{
Context c=new Context();
IStrategy s=new ConcreteStrategyA();
c.SetStrategy(s);
c.StrategyMethod();
Console.WriteLine("-----------------");
s=new ConcreteStrategyB();
c.SetStrategy(s);
c.StrategyMethod();
Console.Read();
}
}


public interface IStrategy
{
void StrategyMethod();
}


public class ConcreteStrategyA : IStrategy
{
public void StrategyMethod()
{
Console.WriteLine("具体策略A的策略方法被访问!");
}
}


public class ConcreteStrategyB : IStrategy
{
public void StrategyMethod()
{
Console.WriteLine("具体策略B的策略方法被访问!");
}
}


public class Context
{
private IStrategy strategy;
public IStrategy GetStrategy()
{
return strategy;
}
public void SetStrategy(IStrategy strategy)
{
this.strategy=strategy;
}
public void StrategyMethod()
{
strategy.StrategyMethod();
}
}

程序运行结果如下:

1
2
3
具体策略A的策略方法被访问!
-----------------
具体策略B的策略方法被访问!

策略模式在很多地方用到,如 Java SE 中的容器布局管理就是一个典型的实例,Java SE 中的每个容器都存在多种布局供用户选择。在程序设计中,通常在以下几种情况中使用策略模式较多:

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
  • 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

在一个使用策略模式的系统中,当存在的策略很多时,客户端管理所有策略算法将变得很复杂,如果在环境类中使用策略工厂模式来管理这些策略类将大大减少客户端的工作复杂度,其结构图如图所示:

在现实世界中,许多对象并不是独立存在的,其中一个对象的行为发生改变可能会导致一个或者多个其他对象的行为也发生改变。例如,某种商品的物价上涨时会导致部分商家高兴,而消费者伤心。

在软件世界也是这样,例如,事件模型中的事件源与事件处理者。所有这些,如果用观察者模式来实现就非常方便。

观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。

观察者模式是一种对象行为型模式,其主要优点如下:

  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系
  • 目标与观察者之间建立了一套触发机制

它的主要缺点如下:

  • 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用
  • 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。

实现观察者模式时要注意具体目标对象和具体观察者对象之间不能直接调用,否则将使两者之间紧密耦合起来,这违反了面向对象的设计原则。

模式的结构#

观察者模式的主要角色如下:

  • 抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法
  • 具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
  • 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
  • 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。

观察者模式的结构图如图所示:

模式的实现#

观察者模式的实现代码如下:

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
class Program
{
static void Main(string[] args)
{
Subject subject=new ConcreteSubject();
IObserver obs1=new ConcreteObserver1();
IObserver obs2=new ConcreteObserver2();
subject.Add(obs1);
subject.Add(obs2);
subject.NotifyObserver();
Console.Read();
}
}


public abstract class Subject
{
protected List<IObserver> observers=new List<IObserver>();

public void Add(IObserver observer)
{
observers.Add(observer);
}

public void Remove(IObserver observer)
{
observers.Remove(observer);
}
public abstract void NotifyObserver();
}


public class ConcreteSubject : Subject
{
public override void NotifyObserver()
{
Console.WriteLine("具体目标发生改变...");
Console.WriteLine("--------------");

foreach (var obs in observers)
{
obs.Response();
}
}
}


public interface IObserver
{
void Response();
}


public class ConcreteObserver1 : IObserver
{
public void Response()
{
Console.WriteLine("具体观察者1作出反应!");
}
}


public class ConcreteObserver2 : IObserver
{
public void Response()
{
Console.WriteLine("具体观察者2作出反应!");
}
}

程序运行结果如下:

1
2
3
4
具体目标发生改变...
--------------
具体观察者1作出反应!
具体观察者2作出反应!

通过前面的分析与应用实例可知观察者模式适合以下几种情形:

  • 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时,可将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。

在.net环境下,其运行时库为开发者提供了IObservable和 IObserver接口,用于实现观察者模式软件设计。另外,ObservableCollection 类表示一个动态数据集合,它可在添加、删除项目或刷新整个列表时提供通知。

  • 提供者或主题,是将通知发送给观察者的对象。 提供程序是实现IObservable 接口的类或结构。 提供者必须实现单个方法IObservable .Subscribe,该方法由希望从提供者接收通知的观察者调用
  • 观察者,即从提供程序接收通知的对象。 观察者是实现 IObserver 接口的类或结构。 观察者必须实现以下三个方法,这三个方法均由提供程序调用
    IObserver.OnNext,它向观察者提供新信息或当前信息。
    IObserver.OnError,它通知观察者已发生错误。
    IObserver.OnCompleted,它指示提供程序已完成发送通知。
  • 允许提供程序跟踪观察者的一种机制。 通常情况下,提供程序使用容器对象(如 List 对象)来保存对已订阅通知的 IObserver 实现的引用。 将存储容器用于此目的使提供程序能够处理零到无限数量的观察者。 未定义观察者接收通知的顺序;提供程序可以随意使用任何方法来确定顺序
  • IDisposable 实现,它使提供程序在能够通知完成时删除观察者。 观察者从 Subscribe 方法接收对 IDisposable 实现的引用,因此它们还可以调用 IDisposable.Dispose 方法,以便在提供程序已完成发送通知之前取消订阅
  • 包含提供程序发送到其观察者的数据的对象。 此对象的类型对应 IObservable 和 IObserver 接口的泛型类型参数。 尽管此对象可与 IObservable 实现相同,但通常情况下,它是一个单独的类型

注:在 Java 中,通过 java.util.Observable 类和 java.util.Observer 接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例。

下面的示例演示观察者设计模式,实现定位系统实时通知当前经纬度坐标,代码如下:

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
class Program
{
static void Main(string[] args)
{

LocationTracker provider = new LocationTracker();
LocationReporter reporter1 = new LocationReporter("FixedGPS");
reporter1.Subscribe(provider);
LocationReporter reporter2 = new LocationReporter("MobileGPS");
reporter2.Subscribe(provider);

provider.TrackLocation(new Location(47.6456, -122.1312));
reporter1.Unsubscribe();
provider.TrackLocation(new Location(47.6677, -122.1199));
provider.TrackLocation(null);
provider.EndTransmission();

Console.Read();
}
}




public struct Location
{
double lat, lon;

public Location(double latitude, double longitude)
{
this.lat = latitude;
this.lon = longitude;
}




public double Latitude
{ get { return this.lat; } }



public double Longitude
{ get { return this.lon; } }
}




public class LocationReporter : IObserver<Location>
{
private IDisposable unsubscriber;
private string instName;

public LocationReporter(string name)
{
this.instName = name;
}

public string Name
{ get { return this.instName; } }





public virtual void Subscribe(IObservable<Location> provider)
{
if (provider != null)
unsubscriber = provider.Subscribe(this);
}

public virtual void OnCompleted()
{
Console.WriteLine("位置跟踪器已将数据传输到 {0}", this.Name);
this.Unsubscribe();
}

public virtual void OnError(Exception e)
{
Console.WriteLine("{0}: 无法确定位置", this.Name);
}

public virtual void OnNext(Location value)
{
Console.WriteLine("{2}: 当前位置是 {0}, {1}", value.Latitude, value.Longitude, this.Name);
}




public virtual void Unsubscribe()
{
unsubscriber.Dispose();
}
}




public class LocationTracker : IObservable<Location>
{
public LocationTracker()
{
observers = new List<IObserver<Location>>();
}

private List<IObserver<Location>> observers;






public IDisposable Subscribe(IObserver<Location> observer)
{
if (!observers.Contains(observer))
observers.Add(observer);
return new Unsubscriber(observers, observer);
}




private class Unsubscriber : IDisposable
{
private List<IObserver<Location>> _observers;
private IObserver<Location> _observer;

public Unsubscriber(List<IObserver<Location>> observers, IObserver<Location> observer)
{
this._observers = observers;
this._observer = observer;
}

public void Dispose()
{
if (_observer != null && _observers.Contains(_observer))
_observers.Remove(_observer);
}
}

public void TrackLocation(Nullable<Location> loc)
{
foreach (var observer in observers)
{
if (!loc.HasValue)
observer.OnError(new LocationUnknownException());
else
observer.OnNext(loc.Value);
}
}

public void EndTransmission()
{
foreach (var observer in observers.ToArray())
{
if (observers.Contains(observer))
observer.OnCompleted();
}
observers.Clear();
}
}




public class LocationUnknownException : Exception
{
internal LocationUnknownException()
{ }
}

程序运行结果如下:

1
2
3
4
5
FixedGPS:当前位置是47.6456,-122.1312
MobileGPS:当前位置是47.6456,-122.1312
MobileGPS:当前位置是47.6677,-122.1199
MobileGPS:无法确定位置位置
跟踪器已将数据传输到MobileGPS

参考资料:
观察者设计模式——MSDN
ObservableCollection 类——MSDN
IObservable 接口——MSDN
IObserver 接口——MSDN

在软件开发中,会遇到有些问题多次重复出现,而且有一定的相似性和规律性。如果将它们归纳成一种简单的语言,那么这些问题实例将是该语言的一些句子,这样就可以用“编译原理”中的解释器模式来实现了。

虽然使用解释器模式的实例不是很多,但对于满足以上特点,且对运行效率要求不是很高的应用实例,如果用解释器模式来实现,其效果是非常好的,本文将介绍其工作原理与使用方法。

解释器(Interpreter)模式的定义:给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。

这里提到的文法和句子的概念同编译原理中的描述相同,“文法”指语言的语法规则,而“句子”是语言集中的元素。例如,汉语中的句子有很多,“我是中国人”是其中的一个句子,可以用一棵语法树来直观地描述语言中的句子。

解释器模式是一种类行为型模式,其主要优点如下:

  • 扩展性好:由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
  • 容易实现:在语法树中的每个表达式节点类都是相似的,所以实现其文法较为容易。

解释器模式的主要缺点如下:

  • 执行效率较低:解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
  • 会引起类膨胀:解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护。
  • 可应用的场景比较少:在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。

解释器模式常用于对简单语言的编译或分析实例中,为了掌握好它的结构与实现,必须先了解编译原理中的“文法、句子、语法树”等相关概念。

文法:文法是用于描述语言的语法结构的形式规则。没有规矩不成方圆,任何事情都要有规则,语言也一样,不管它是机器语言还是自然语言,都有它自己的文法规则。例如,中文中的“句子”的文法如下:

1
2
3
4
5
6
7
〈句子〉::=〈主语〉〈谓语〉〈宾语〉
〈主语〉::=〈代词〉|〈名词〉
〈谓语〉::=〈动词〉
〈宾语〉::=〈代词〉|〈名词〉
〈代词〉你|我|他
〈名词〉7大学生I筱霞I英语
〈动词〉::=是|学习

注:这里的符号“::=”表示“定义为”的意思,用“〈”和“〉”括住的是非终结符,没有括住的是终结符。

句子:句子是语言的基本单位,是语言集中的一个元素,它由终结符构成,能由“文法”推导出。例如,上述文法可以推出“我是大学生”,所以它是句子。

语法树:语法树是句子结构的一种树型表示,它代表了句子的推导结果,它有利于理解句子语法结构的层次。下图所示是“我是大学生”的语法树:

有了以上基础知识,现在来介绍解释器模式的结构就简单了。解释器模式的结构与组合模式相似,不过其包含的组成元素比组合模式多,而且组合模式是对象结构型模式,而解释器模式是类行为型模式。

模式的结构#

解释器模式包含以下主要角色:

  • 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 Interpret()。
  • 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
  • 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
  • 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
  • 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。

解释器模式的结构图如图所示:

模式的实现#

解释器模式实现的关键是定义文法规则、设计终结符类与非终结符类、画出结构图,必要时构建语法树,其代码结构如下:

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

public interface IAbstractExpression
{
Object Interpret(String info);
}


public class TerminalExpression : IAbstractExpression
{
public Object Interpret(String info)
{

}
}


public class NonterminalExpression : IAbstractExpression
{
private IAbstractExpression exp1;
private IAbstractExpression exp2;
public Object Interpret(String info)
{

}
}


public class Context
{
private IAbstractExpression exp;
public Context()
{

}
public void Operation(String info)
{

}
}

前面介绍了解释器模式的结构与特点,下面分析它的应用场景:

  • 当语言的文法较为简单,且执行效率不是关键问题时。
  • 当问题重复出现,且可以用一种简单的语言来进行表达时。
  • 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候,如 XML 文档解释。

注意:解释器模式在实际的软件开发中使用比较少,因为它会引起效率、性能以及维护等问题。如果碰到对表达式的解释,在 C# 中可以用 Expression类 或 Flee 等来设计

在项目开发中,如果要对数据表达式进行分析与计算,无须再用解释器模式进行设计了,C# 提供了 Expression 表达式树,也可以使用 Flee 等开源类库,它们可以解释一些复杂的文法,功能强大,使用简单。

Flee是.NET框架的表达式解析器和评估器,它使用自定义编译器,强类型表达式语言和轻量级代码生成器将表达式直接编译为IL。
github链接:Flee
使用说明:https://github.com/mparlak/Flee/wiki

使用NuGet安装Flee,创建和评估表达式的示例代码如下:

1
2
3
4
5
6
7
8
9
10

CultureInfo ci = new CultureInfo("fr-FR");

ExpressionContext context = new ExpressionContext();
context.ParseCulture = ci
context.Imports.AddType(typeof(Math));


IDynamicExpression e = context.CompileDynamic("round(100,75; 1)");
object result = e.Evaluate();

在现实生活中,有些集合对象中存在多种不同的元素,且每种元素也存在多种不同的访问者和处理方式。例如,公园中存在多个景点,也存在多个游客,不同的游客对同一个景点的评价可能不同。

这些被处理的数据元素相对稳定而访问方式多种多样的数据结构,如果用“访问者模式”来处理比较方便。

访问者模式能把处理方法从数据结构中分离出来,并可以根据需要增加新的处理方法,且不用修改原来的程序代码与数据结构,这提高了程序的扩展性和灵活性。

访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式

访问者(Visitor)模式是一种对象行为型模式,其主要优点如下:

  • 扩展性好:能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  • 复用性好:可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
  • 灵活性好:访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
  • 符合单一职责原则:访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。

访问者(Visitor)模式的主要缺点如下:

  • 增加新的元素类很困难:在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  • 破坏封装:访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
  • 违反了依赖倒置原则:访问者模式依赖了具体类,而没有依赖抽象类。

访问者(Visitor)模式实现的关键是如何将作用于元素的操作分离出来封装成独立的类,其基本结构与实现方法如下。

模式的结构#

访问者模式包含以下主要角色:

  • 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 Visit() ,该操作中的参数类型标识了被访问的具体元素。
  • 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
  • 抽象元素(Element)角色:声明一个包含接受操作 Accept() 的接口,被接受的访问者对象作为 Accept() 方法的参数。
  • 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 Accept() 操作,其方法体通常都是 visitor.Visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
  • 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。

其结构图如图所示:

模式的实现#

访问者模式的实现代码如下:

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
class Program
{
static void Main(string[] args)
{
ObjectStructure os=new ObjectStructure();
os.Add(new ConcreteElementA());
os.Add(new ConcreteElementB());
IVisitor visitor=new ConcreteVisitorA();
os.Accept(visitor);
Console.WriteLine("------------------------");
visitor=new ConcreteVisitorB();
os.Accept(visitor);

Console.Read();
}
}


public interface IVisitor
{
void Visit(ConcreteElementA element);
void Visit(ConcreteElementB element);
}


public class ConcreteVisitorA : IVisitor
{
public void Visit(ConcreteElementA element)
{
Console.WriteLine("具体访问者A访问-->"+element.OperationA());
}
public void Visit(ConcreteElementB element)
{
Console.WriteLine("具体访问者A访问-->"+element.OperationB());
}
}


public class ConcreteVisitorB : IVisitor
{
public void Visit(ConcreteElementA element)
{
Console.WriteLine("具体访问者B访问-->"+element.OperationA());
}
public void Visit(ConcreteElementB element)
{
Console.WriteLine("具体访问者B访问-->"+element.OperationB());
}
}


public interface IElement
{
void Accept(IVisitor visitor);
}


public class ConcreteElementA : IElement
{
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
public String OperationA()
{
return "具体元素A的操作。";
}
}


public class ConcreteElementB : IElement
{
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
public String OperationB()
{
return "具体元素B的操作。";
}
}


public class ObjectStructure
{
private List<IElement> list=new List<IElement>();
public void Accept(IVisitor visitor)
{
foreach (var item in list)
{
item.Accept(visitor);
}
}
public void Add(IElement element)
{
list.Add(element);
}
public void Remove(IElement element)
{
list.Remove(element);
}
}

程序的运行结果如下:

1
2
3
4
5
具体访问者A访问
具体访问者A访问

具体访问者B访问
具体访问者B访问

通常在以下情况可以考虑使用访问者(Visitor)模式:

  • 对象结构相对稳定,但其操作算法经常变化的程序。
  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
  • 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。

访问者(Visitor)模式是使用频率较高的一种设计模式,它常常同以下两种设计模式联用:

  • 与“迭代器模式”联用:因为访问者模式中的“对象结构”是一个包含元素角色的容器,当访问者遍历容器中的所有元素时,常常要用迭代器。
    注:现在一般的高级语言都自带的有迭代器,不需要自己实现。

  • 访问者(Visitor)模式同“组合模式”联用:因为访问者(Visitor)模式中的“元素对象”可能是叶子对象或者是容器对象,如果元素对象包含容器对象,就必须用到组合模式,其结构图如图所示:

在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同,如找领导出差报销等。
在计算机软硬件中也有相关例子,如总线网中数据报传送、异常处理。所有这些,如果用责任链模式都能很好解决。

责任链(Chain of Responsibility)模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链。当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

注意:责任链模式也叫职责链模式

在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,所以责任链将请求的发送者和请求的处理者解耦了。

责任链模式是一种对象行为型模式,其主要优点如下:

  • 降低了对象之间的耦合度:该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
  • 增强了系统的可扩展性:可以根据需要增加新的请求处理类,满足开闭原则。
  • 增强了给对象指派职责的灵活性:当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
  • 责任链简化了对象之间的连接:每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
  • 责任分担:每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。

其主要缺点如下:

  • 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
  • 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
  • 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。

通常情况下,可以通过数据链表来实现职责链模式的数据结构。

模式的结构#

职责链模式主要包含以下角色:

  • 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  • 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  • 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

其结构图如图所示:

客户端可按图所示设置责任链:

模式的实现#

职责链模式的实现代码如下:

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
class Program
{
static void Main(string[] args)
{

Handler handler1=new ConcreteHandler1();
Handler handler2=new ConcreteHandler2();
handler1.SetNext(handler2);

handler1.HandleRequest("two");
Console.Read();
}
}


public abstract class Handler
{
private Handler next;
public void SetNext(Handler next)
{
this.next=next;
}
public Handler GetNext()
{
return next;
}

public abstract void HandleRequest(String request);
}


public class ConcreteHandler1 : Handler
{
public override void HandleRequest(String request)
{
if(request.Equals("one"))
{
Console.WriteLine("具体处理者1负责处理该请求!");
}
else
{
if(GetNext()!=null)
{
GetNext().HandleRequest(request);
}
else
{
Console.WriteLine("没有人处理该请求!");
}
}
}
}


public class ConcreteHandler2 : Handler
{
public override void HandleRequest(String request)
{
if(request.Equals("two"))
{
Console.WriteLine("具体处理者2负责处理该请求!");
}
else
{
if(GetNext()!=null)
{
GetNext().HandleRequest(request);
}
else
{
Console.WriteLine("没有人处理该请求!");
}
}
}
}

程序运行结果如下:

1
具体处理者2负责处理该请求!

前边已经讲述了关于责任链模式的结构与特点,下面介绍其应用场景,责任链模式通常在以下几种情况使用:

  • 有多个对象可以处理一个请求,哪个对象处理该请求由运行时刻自动确定。
  • 可动态指定一组对象处理请求,或添加新的处理者。
  • 在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。

职责链模式存在以下两种情况:

  • 纯的职责链模式:一个请求必须被某一个处理者对象所接收,且一个具体处理者对某个请求的处理只能采用以下两种行为之一:自己处理(承担责任);把责任推给下家处理。
  • 不纯的职责链模式:允许出现某一个具体处理者对象在承担了请求的一部分责任后又将剩余的责任传给下家的情况,且一个请求可以最终不被任何接收端对象所接收。

在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。

既然将遍历方法封装在聚合类中不可取,那么聚合类中不提供遍历方法,将遍历方法由用户自己实现是否可行呢?答案是同样不可取,因为这种方式会存在两个缺点:

  • 暴露了聚合类的内部表示,使其数据不安全。
  • 增加了客户的负担。

“迭代器模式”能较好地克服以上缺点,它在客户访问类与聚合类之间插入一个迭代器,这分离了聚合对象与其遍历行为,对客户也隐藏了其内部细节,且满足“单一职责原则”和“开闭原则”。

迭代器(Iterator)模式的定义:提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
迭代器模式是一种对象行为型模式,其主要优点如下:

  • 访问一个聚合对象的内容而无须暴露它的内部表示。
  • 遍历任务交由迭代器完成,这简化了聚合类。
  • 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
  • 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
  • 封装性良好,为遍历不同的聚合结构提供一个统一的接口。

其主要缺点是:增加了类的个数,这在一定程度上增加了系统的复杂性。

迭代器模式是通过将聚合对象的遍历行为分离出来,抽象成迭代器类来实现的,其目的是在不暴露聚合对象的内部结构的情况下,让外部代码透明地访问聚合的内部数据。

模式的结构#

迭代器模式主要包含以下角色:

  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 HasNext()、First()、Next() 等方法。
  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

其结构图如图所示:

模式的实现#

迭代器模式的实现代码如下:

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
86
87
88
89
90
91
class Program
{
static void Main(string[] args)
{
IAggregate ag = new ConcreteAggregate();
ag.Add("中山大学");
ag.Add("华南理工");
ag.Add("韶关学院");
Console.WriteLine("聚合的内容有:");
IIterator it = ag.GetIterator();
while (it.HasNext())
{
Object ob = it.Next();
Console.WriteLine(ob.ToString());
}
Console.WriteLine("First:" + it.First().ToString());

Console.Read();
}
}


public interface IAggregate
{
void Add(Object obj);
void Remove(Object obj);
IIterator GetIterator();
}


public class ConcreteAggregate : IAggregate
{
private List<Object> list=new List<Object>();
public void Add(Object obj)
{
list.Add(obj);
}
public void Remove(Object obj)
{
list.Remove(obj);
}
public IIterator GetIterator()
{
return(new ConcreteIterator(list));
}
}


public interface IIterator
{
Object First();
Object Next();
bool HasNext();
}


public class ConcreteIterator : IIterator
{
private List<Object> list=null;
private int index=-1;
public ConcreteIterator(List<Object> list)
{
this.list=list;
}
public bool HasNext()
{
if(index<list.Count-1)
{
return true;
}
else
{
return false;
}
}
public Object First()
{
index=0;
Object obj=list[index];;
return obj;
}
public Object Next()
{
Object obj=null;
if(this.HasNext())
{
obj=list[++index];
}
return obj;
}
}

程序运行结果如下:

1
2
3
4
5
聚合的内容有:
中山大学
华南理工
韶关学院
First:中山大学

注:在C#中,推荐使用泛型代替Object以降低性能消耗。

前面介绍了关于迭代器模式的结构与特点,下面介绍其应用场景,迭代器模式通常在以下几种情况使用:

  • 当需要为聚合对象提供多种遍历方式时。
  • 当需要为遍历不同的聚合结构提供一个统一的接口时。
  • 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。

由于聚合与迭代器的关系非常密切,所以大多数语言在实现聚合类时都提供了迭代器类,因此大数情况下使用语言中已有的聚合类的迭代器就已经够了。

迭代器模式常常与组合模式结合起来使用,在对组合模式中的容器构件进行访问时,经常将迭代器潜藏在组合模式的容器构成类中。当然,也可以构造一个外部迭代器来对容器构件进行访问,其结构图如图所示: