0%

在现实生活中,经常出现两个对象因接口不兼容而不能在一起工作的实例,这时需要第三者进行适配。例如,用直流电的笔记本电脑接交流电源时需要一个电源适配器。
在软件设计中也可能出现:需要开发的具有某种业务功能的组件在现有的组件库中已经存在,但它们与当前系统的接口规范不兼容,如果重新开发这些组件成本又很高,这时用适配器模式能很好地解决这些问题。

适配器模式(Adapter)的定义如下:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

该模式的主要优点如下:

  • 客户端通过适配器可以透明地调用目标接口
  • 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
  • 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。

其缺点是:对类适配器来说,更换适配器的实现过程比较复杂

类适配器模式可采用多重继承方式实现,如 C++ 可定义一个适配器类来同时继承当前系统的业务接口和现有组件库中已经存在的组件接口;C#、Java 不支持多继承,但可以定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。

对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。

模式的结构#

适配器模式(Adapter)包含以下主要角色:

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者

类适配器模式的结构图如图所示:

对象适配器模式的结构图如图所示:

模式的实现#

类适配器模式的代码如下:

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

public class Program
{
static void Main(string[] args)
{

Console.WriteLine("类适配器模式测试:");
ITarget target = new ClassAdapter();
target.Request();
Console.ReadLine();
}
}


public interface ITarget
{
void Request();
}


public class Adaptee
{
public void SpecificRequest()
{
Console.WriteLine("适配者中的业务代码被调用!");
}
}


public class ClassAdapter : Adaptee,ITarget
{
public void Request()
{
SpecificRequest();
}
}

程序的运行结果如下:

1
2
类适配器模式测试:
适配者中的业务代码被调用!

对象适配器模式的代码如下:

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

public class Program
{
static void Main(string[] args)
{

Console.WriteLine("类适配器模式测试:");
Adaptee adaptee = new Adaptee();
ITarget target = new ObjectAdapter(adaptee);
target.Request();
Console.ReadLine();
}
}


class ObjectAdapter : ITarget
{
private Adaptee adaptee;
public ObjectAdapter(Adaptee adaptee)
{
this.adaptee=adaptee;
}
public void Request()
{
adaptee.SpecificRequest();
}
}

说明:对象适配器模式中的“目标接口”和“适配者类”的代码同类适配器模式一样,只要修改适配器类和客户端的代码即可。

程序的运行结果如下:

1
2
类适配器模式测试:
适配者中的业务代码被调用!

适配器模式(Adapter)通常适用于以下场景:

  • 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
  • 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。

适配器模式(Adapter)可扩展为双向适配器模式,双向适配器类既可以把适配者接口转换成目标接口,也可以把目标接口转换成适配者接口,其结构图如图所示:

代码如下:

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

public class Program
{
static void Main(string[] args)
{

Console.WriteLine("类适配器模式测试:");
ITwoWayTarget target = new TargetRealize();
ITwoWayAdaptee adaptee = new AdapteeRealize();
Console.WriteLine("目标通过双向适配器访问适配者:");
ITwoWayTarget twoWayTarget = new TwoWayAdapter(adaptee);
twoWayTarget.Request();
Console.WriteLine("-------------------");
Console.WriteLine("适配者通过双向适配器访问目标:");
ITwoWayAdaptee twoWayAdaptee = new TwoWayAdapter(target);
twoWayAdaptee.SpecificRequest();
Console.ReadLine();
}
}


public interface ITwoWayTarget
{
void Request();
}


public interface ITwoWayAdaptee
{
void SpecificRequest();
}


public class TargetRealize :ITwoWayTarget
{
public void Request()
{
Console.WriteLine("目标代码被调用!");
}
}


public class AdapteeRealize : ITwoWayAdaptee
{
public void SpecificRequest()
{
Console.WriteLine("适配者代码被调用!");
}
}


public class TwoWayAdapter : ITwoWayTarget, ITwoWayAdaptee
{
private ITwoWayTarget target;
private ITwoWayAdaptee adaptee;
public TwoWayAdapter(ITwoWayTarget target)
{
this.target=target;
}
public TwoWayAdapter(ITwoWayAdaptee adaptee)
{
this.adaptee=adaptee;
}
public void Request()
{
adaptee.SpecificRequest();
}
public void SpecificRequest()
{
target.Request();
}
}

在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。
如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。
例如,你刚刚参力口工作想租房,可以找“房屋中介”;或者,自己刚刚到一个陌生城市找工作,可以找“人才交流中心”帮忙。
在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;还有大家常用的 QQ 聊天程序的“中介者”是 QQ 服务器。
所有这些,都可以采用“中介者模式”来实现,它将大大降低对象之间的耦合性,提高系统的灵活性。

中介者(Mediator)模式的定义:定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用

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

  • 降低了对象之间的耦合性,使得对象易于独立地被复用。
  • 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

其主要缺点是:当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。

中介者模式实现的关键是找出“中介者”,下面对它的结构和实现进行分析。

模式的结构

中介者模式包含以下主要角色:

  • 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
  • 具体中介者(Concrete Mediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
  • 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
  • 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。

中介者模式的结构图如图所示:

模式的实现

中介者模式的实现代码如下:

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
class Program
{
static void Main(string[] args)
{
Mediator md=new ConcreteMediator();
Colleague c1,c2;
c1=new ConcreteColleague1();
c2=new ConcreteColleague2();
md.Register(c1);
md.Register(c2);
c1.Send();
Console.WriteLine("-------------");
c2.Send();

Console.Read();
}
}


public abstract class Mediator
{

public abstract void Register(Colleague colleague);

public abstract void Relay(Colleague colleague);
}


public class ConcreteMediator : Mediator
{
private List<Colleague> colleagues=new List<Colleague>();
public override void Register(Colleague colleague)
{
if(!colleagues.Contains(colleague))
{
colleagues.Add(colleague);
colleague.SetMediator(this);
}
}
public override void Relay(Colleague colleague)
{
foreach (var item in colleagues)
{
if (!item.Equals(colleague))
{
item.Receive();
}
}
}
}


public abstract class Colleague
{
protected Mediator mediator;
public void SetMediator(Mediator mediator)
{
this.mediator=mediator;
}
public abstract void Receive();
public abstract void Send();
}


public class ConcreteColleague1 : Colleague
{
public override void Receive()
{
Console.WriteLine("具体同事类1收到请求。");
}
public override void Send()
{
Console.WriteLine("具体同事类1发出请求。");
mediator.Relay(this);
}
}


public class ConcreteColleague2 : Colleague
{
public override void Receive()
{
Console.WriteLine("具体同事类2收到请求。");
}
public override void Send()
{
Console.WriteLine("具体同事类2发出请求。");
mediator.Relay(this);
}
}

程序的运行结果如下:

1
2
3
4
5
具体同事类1发出请求。
具体同事类2收到请求。
-------------
具体同事类2发出请求。
具体同事类1收到请求。

前面分析了中介者模式的结构与特点,下面分析其以下应用场景:

  • 当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
  • 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

在实际开发中,通常采用以下两种方法来简化中介者模式,使开发变得更简单:

  • 不定义中介者接口,把具体中介者对象实现成为单例。
  • 同事对象不持有中介者,而是在需要的时直接获取中介者对象并调用。

下图所示是简化中介者模式的结构图:

程序代码如下:

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
class Program
{
static void Main(string[] args)
{
ISimpleColleague c1,c2;
c1=new SimpleConcreteColleague1();
c2=new SimpleConcreteColleague2();
c1.Send();
Console.WriteLine("-----------------");
c2.Send();
Console.Read();
}
}


public class SimpleMediator
{
private static SimpleMediator smd=new SimpleMediator();
private List<ISimpleColleague> colleagues=new List<ISimpleColleague>();
private SimpleMediator(){}
public static SimpleMediator GetMediator()
{
return smd;
}
public void Register(ISimpleColleague colleague)
{
if(!colleagues.Contains(colleague))
{
colleagues.Add(colleague);
}
}
public void Relay(ISimpleColleague scl)
{
foreach (var item in colleagues)
{
if (!item.Equals(scl))
{
item.Receive();
}
}
}
}

public interface ISimpleColleague
{
void Receive();
void Send();
}


public class SimpleConcreteColleague1 : ISimpleColleague
{
public SimpleConcreteColleague1()
{
SimpleMediator smd=SimpleMediator.GetMediator();
smd.Register(this);
}
public void Receive()
{
Console.WriteLine("具体同事类1:收到请求。");
}
public void Send()
{
SimpleMediator smd=SimpleMediator.GetMediator();
Console.WriteLine("具体同事类1:发出请求...");
smd.Relay(this);
}
}


public class SimpleConcreteColleague2 : ISimpleColleague
{
public SimpleConcreteColleague2()
{
SimpleMediator smd=SimpleMediator.GetMediator();
smd.Register(this);
}
public void Receive()
{
Console.WriteLine("具体同事类2:收到请求。");
}
public void Send()
{
SimpleMediator smd=SimpleMediator.GetMediator();
Console.WriteLine("具体同事类2:发出请求...");
smd.Relay(this);
}
}

程序运行结果如下:

1
2
3
4
5
具体同事类1:发出请求...
具体同事类2:收到请求。
-----------------
具体同事类2:发出请求...
具体同事类1:收到请求。

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

命令(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、记事本、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

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

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

策略(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();