观察者模式

思考并回答以下问题:

本章导学

在软件系统中,对象并不是孤立存在的,一个对象行为的改变可能会导致一个或多个其他与之存在依赖关系的对象行为发生改变。观察者模式用于描述对象之间的依赖关系,为实现多个对象之间的联动提供了一种解决方案,它是一种使用频率非常高的设计模式。

本章将学习观察者模式的定义与结构,分析观察者模式的实现原理,通过实例学习如何编程实现观察者模式并理解观察者模式在.NET事件处理中的应用。

本章知识点

  • 观察者模式的定义
  • 观察者模式的结构
  • 观察者模式的实现
  • 观察者模式的应用
  • 观察者模式的优缺点
  • 观察者模式的适用环境
  • 观察者模式与.NET中的委托事件模型

观察者模式概述

“红灯停,绿灯行”,在日常生活中,交通信号灯装点着城市,指挥着日益拥挤的城市交通。当红灯亮起,来往的汽车将停止;而绿灯亮起,汽车可以继续前行。在这个过程中,交通信号灯是汽车(更准确地说应该是汽车驾驶员)的观察目标,而汽车是观察者。随着交通信号灯的变化,汽车的行为也将随之变化,一盏交通信号灯可以指挥多辆汽车。交通信号与汽车示意图如图1所示。

图1 交通信号与汽车示意图

在软件系统中,有些对象之间也存在着类似交通信号灯和汽车之间的关系,一个对象的状态或行为的变化将导致其他对象的状态或行为也发生改变,它们之间将产生联动,正所谓“牵一发而动全身”。为了更好地描述对象之间存在的这种一对多(包括一对一)的联动,观察者模式应运而生,它定义了对象之间一种一对多的依赖关系,让一个对象的改变能够影响其他对象。

观察者模式是使用频率较高的设计模式之一,用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。在观察者模式中,发生改变的对象称为观察目标,被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间可以没有任何相互联系,用户可以根据需要增加和删除观察者,使得系统更易于扩展。

观察者模式的定义如下:

1
定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象都得到通知并被自动更新。

观察者模式又称为发布-订阅(Publish-Subscribe)模式、模型-视图(Model-View)模式、源-监听器(Source-Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。

观察者模式的结构与实现

观察者模式的结构

观察者模式结构中通常包括观察目标和观察者两个继承层次结构,其结构如图2所示。

图2 观察者模式结构图

由图2可知,观察者模式包含以下4个角色。

(1)Subject(目标):目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以被任意数量的观察者观察,它提供一系列方法来增加和删除观察者对象,同时定义了通知方法Notify()。目标类可以是接口,也可以是抽象类或具体类。

(2)ConcreteSubject(具体目标):具体目标是目标类的子类,通常包含经常发生改变的数据,当它的状态发生改变时,将向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务方法(如果有)。如果无须扩展目标类,具体目标类则可以省略。

(3)Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法Update(),因此又称为抽象观察者。

(4)ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的Update()方法。通常在实现时,可以调用具体目标类的Attach()方法将自己添加到目标类的集合中或通过Detach()方法将自己从目标类的集合中删除。

观察者模式的实现

观察者模式描述了如何建立对象与对象之间的依赖关系,以及如何构造满足这种需求的系统。观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。作为对这个通知的响应,每个观察者都将监视观察目标的状态,以使其状态与目标状态同步,这种交互也称为发布-订阅(Publish-Subscribe),观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。下面通过演示代码来对观察者模式进行进一步分析。首先定义一个抽象目标类Subject,其典型代码如下:

Subject.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Collections;

abstract class Subject
{
// 定义一个观察者集合用于存储所有观察者对象
protected ArrayList observers = new ArrayList();
// 声明抽象注册方法,用于向观察者集合中增加一个观察者
public abstract void Attach(Observer observer);
// 声明抽象注销方法,用于在观察者集合中删除一个观察者
public abstract void Detach(Observer observer);
// 声明抽象通知方法
public abstract void Notify();
}

具体目标类ConcreteSubject是实现了抽象目标类Subject的一个具体子类,它实现了上述3个方法,其典型代码如下:

ConcreteSubject.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ConcreteSubject : Subject
{
public override void Attach(Observer observer)
{
observers.Add(observer);
}

public override void Detach(Observer observer)
{
observers.Remove(observer);
}

// 实现通知方法
public override void Notify()
{
// 遍历观察者集合,调用每一个观察者的响应方法
foreach(object obs in observers)
{
((Observer)obs).Update();
}
}
}

抽象观察者角色一般定义为一个接口,通常只声明一个Update()方法,为不同观察者的更新(响应)行为定义相同的接口,这个方法在其子类中实现,不同的观察者具有不同的响应方法。抽象观察者Observer的典型代码如下:

Observer.cs

1
2
3
4
interface Observer
{
void Update();
}

在具体观察者ConcreteObserver中实现了Update()方法,其典型代码如下:

ConcreteObserver.cs

1
2
3
4
5
6
7
8
class ConcreteObserver : Observer
{
// 实现响应方法
public void Update()
{
// 具体更新代码
}
}

在有些更加复杂的情况下,具体观察者类ConcreteObserver的Update()方法在执行时需要使用到具体目标类ConcreteSubject中的状态(属性),因此在ConcreteObserver与ConcreteSubject之间有时候还存在关联或依赖关系,在ConcreteObserver中定义一个ConcreteSubject实例,通过该实例获取存储在ConcreteSubject中的状态。如果ConcreteObserver的Update()方法不需要使用ConcreteSubject中的状态属性,则可以对观察者模式的标准结构进行简化,在具体观察者ConcreteObserver和具体目标ConcreteSubject之间无须维持对象引用。

如果在具体层之间具有关联关系,系统的扩展性将受到一定的影响,增加新的具体目标类有时候需要修改原有观察者的代码,在一定程度上违背了开闭原则,如果原有观察者类无须关联新增的具体目标,则系统扩展性不受影响。

在客户端代码中,首先创建具体目标对象以及具体观察者对象,然后调用目标对象的Attach()方法,将这个观察者对象在目标对象中登记,也就是将它加入到目标对象的观察者集合中,代码片段如下:

1
2
3
4
5
6
...
Subject subject = new ConcreteSubject();
Observer observer = new ConcreteObserver();
subject.Attach(observer);
subject.Notify();
...

客户端在调用目标对象的Notify()方法时,将调用在其观察者集合中注册的观察者对象的Update()方法。

观察者模式的应用实例

下面通过一个应用实例来进一步学习和理解观察者模式。

1.实例说明

在某多人联机对战游戏中,多个玩家可以加入同一战队组成联盟,当战队中的某一成员受到敌人攻击时将给所有其他盟友发送通知,盟友收到通知后将做出响应。

现使用观察者模式设计并实现该过程,以实现战队成员之间的联动。

2.实例类图

通过分析,不难发现在该系统中战队成员之间的联动过程可以简单描述如下:

联盟成员受到攻击->发送通知给盟友->盟友做出响应。

如果按照上述思路来设计系统,一个战队联盟成员在受到攻击时需要通知他的每一位盟友,每个联盟成员都需要持有其他所有盟友的信息,这将导致系统开销较大,因此可以引入一个新的角色——指挥部(战队控制中心)来负责维护和管理每个战队所有成员的信息。当一个联盟成员受到攻击时,将向对应的指挥部发送请求信息,指挥部逐一通知每个盟友,盟友再做出响应,如图3所示。

图3 多人联机对战游戏中对象的联动

在图3中,受攻击的联盟成员将与指挥部产生联动,指挥部还将与其他盟友产生联动。

通过分析,本实例的结构如图4所示。

图4 多人联机对战游戏结构图

在图4中,AllyControlCenter充当抽象目标类,ConcreteAllyControlCenter充当具体目标类,IObserver充当抽象观察者,Player充当具体观察者。

3.实例代码

(1)AllyControlCenter:指挥部(战队控制中心类),充当抽象目标类。

AllyControlCenter.cs

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
using System;
using System.Collections.Generic;

namespace ObserverSample
{
abstract class AllyControlCenter
{
protected string allyName; // 战队名称
protected List<IObserver> players = new List<IObserver>(); // 定义一个集合用于存储战队成员

public void SetAllyName(string allyName)
{
this.allyName = allyName;
}

public string GetAllyName()
{
return this.allyName;
}

// 注册方法
public void Join(IObserver obs)
{
Console.WriteLine("{0}加入{1}战队!", obs.Name, this.allyName);
players.Add(obs);
}

// 注销方法
public void Quit(IObserver obs)
{
Console.WriteLine("{0}退出{1}战队!", obs.Name, this.allyName);
players.Remove(obs);
}

// 声明抽象通知方法
public abstract void NotifyObserver(string name);
}
}

(2)ConcreteAllyControlCenter:具体指挥部类,充当具体目标类。

ConcreteAllyControlCenter.cs

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
using System;

namespace ObserverSample
{
class ConcreteAllyControlCenter : AllyControlCenter
{
public ConcreteAllyControlCenter(string allyName)
{
Console.WriteLine("{0}战队组建成功!", allyName);
Console.WriteLine("----------------------------");
this.allyName = allyName;
}

// 实现通知方法
public override void NotifyObserver(string name)
{
Console.WriteLine("{0}战队紧急通知,盟友{1}遭受敌人攻击!", this.allyName, name);
// 遍历观察者集合,调用每一个盟友(自己除外)的支援方法
foreach(object obs in players)
{
if (!((IObserver)obs).Name.Equals(name))
{
((IObserver)obs).Help();
}
}
}
}
}

(3)IObserver:抽象观察者类。

IObserver.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace ObserverSample
{
interface IObserver
{
string Name
{
get;
set;
}

void Help(); // 声明支援盟友方法
void BeAttacked(AllyControlCenter acc); // 声明遭受攻击方法
}
}

(4)Player:战队成员类,充当具体观察者类。

Player.cs

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
using System;

namespace ObserverSample
{
class Player : IObserver
{
private string name;

public Player(string name)
{
this.name = name;
}

public string Name
{
get { return name; }
set { name = value; }
}

// 支援盟友方法的实现
public void Help()
{
Console.WriteLine("坚持住,{0}来救你!", this.name);
}

// 遭受攻击方法的实现,当遭受攻击时将调用战队控制中心类的通知方法NotifyObserver()来通知盟友
public void BeAttacked(AllyControlCenter acc)
{
Console.WriteLine("{0}被攻击!", this.name);
acc.NotifyObserver(name);
}
}
}

(5)Program:客户端测试类。

Program.cs

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
using System;

namespace ObserverSample
{
class Program
{
static void Main(string[] args)
{
// 定义观察目标对象
AllyControlCenter acc;
acc = new ConcreteAllyControlCenter("金庸群侠");

// 定义四个观察者对象
IObserver player1, player2, player3, player4;

player1 = new Player("杨过");
acc.Join(player1);

player2 = new Player("令狐冲");
acc.Join(player2);

player3 = new Player("张无忌");
acc.Join(player3);

player4 = new Player("段誉");
acc.Join(player4);

// 某成员遭受攻击
player1.BeAttacked(acc);

Console.Read();
}
}
}

4.结果及分析

编译并运行程序,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
金庸群侠战队组建成功!
----------------------------
杨过加入金庸群侠战队!
令狐冲加入金庸群侠战队!
张无忌加入金庸群侠战队!
段誉加入金庸群侠战队!
杨过被攻击!
金庸群侠战队紧急通知,盟友杨过遭受敌人攻击!
坚持住,令狐冲来救你!
坚持住,张无忌来救你!
坚持住,段誉来救你!

在本实例中,实现了两次对象之间的联动,当一个游戏玩家Player对象的BeAttacked()方法被调用时,将调用指挥部AllyControlCenter的NotifyObserver()方法来进行处理,而在NotifyObserver()方法中又将调用其他Player对象的Help()方法。Player的BeAttacked()方法、AllyControlCenter的NotifyObserver()方法以及Player的Help()方法构成了一个联动触发链,执行顺序如下:

Player.BeAttacked()->AllyControlCenter.NotifyObserver()->Player.Help()。

观察者模式与.NET中的委托事件模型

.NET中的委托事件模型是观察者模式在.NET中的经典应用。在WinForm编程中需要编写事件处理程序对所发生的事件(例如鼠标单击、菜单项选取等)做出反应,并执行相应的操作。事件被触发后,将执行响应该事件的一个或多个事件处理程序,可以将一个事件分配给多个事件处理程序(注册),还可以根据需要动态更改处理事件的方法。产生事件的对象(例如按钮、文本框、菜单等)称为事件的发送者(事件源对象),接收并响应事件的对象称为事件的接收者(事件处理对象)。与观察者模式相对应,事件源对象充当观察目标角色,事件处理对象充当具体观察者角色,如果事件源对象的某个事件触发,则调用事件处理对象中的事件处理程序对事件进行处理。

在.NET中,如果需要从WinForm控件获取事件,先提供一个委托(Delegate)类型的Event Handler,然后将它注册到事件源。在这里委托对象充当了抽象观察者的角色,所有事件处理方法都必须和委托方法具有相同的函数签名。

C#事件注册方法的语法如下:

1
eventSource.someEvent += new SomeEventHandler(someMethod);

在该语法中,eventSource表示事件源,someEvent表示定义在事件源中的事件,SomeEventHandler表示用于处理事件的委托,someMethod表示与委托SomeEventHandler具有相同函数签名的事件处理方法。用户只需修改someMethod,即可实现相同的事件对应不同的事件处理程序。

在.NET事件中,事件源并不需要知道哪些对象或方法会收到将要发生的通知,它只持有与签名相符合的方法的引用,即委托;还可以通过多重传送事件来实现一个事件有多个订阅者,即通过委托将多个方法添加到该事件中,当该事件被触发时,同时执行对应的多个事件处理方法。

下面通过一个简单的自定义事件来进一步说明.NET事件中的观察者模式。首先定义一个包含委托和事件的类EventTest,代码如下:

EventTest.cs

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
using System;

namespace ObserverExtend
{
class EventTest
{
// 定义一个委托
public delegate void UserInput(object sender, EventArgs e);

// 定义一个此委托类型的事件
public event UserInput OnUserInput;

// 模拟事件触发,当输入“0”时引发事件
public void Method()
{
bool flag = false;
Console.WriteLine("请输入数字:");

while (!flag)
{
if (Console.ReadLine() == "0")
{
OnUserInput(this, new EventArgs());
}
}
}
}
}

在类EventTest中定义了一个委托UserInput和一个事件OnUserInput,EventTest充当观察目标类的角色,而委托充当抽象观察者角色,在方法Method()中引发了事件,即调用与委托具有相同函数签名的方法,方法Method()即为目标类的通知方法。

在客户端测试类Program中提供了具体的事件处理方法,并将该方法和事件绑定在一起,这个过程称为订阅事件。Program类的代码如下:

Program.cs

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
using System;

namespace ObserverExtend
{
class Program
{
public Program(EventTest test)
{
// 注册事件或订阅事件
test.OnUserInput += new EventTest.UserInput(Handler);
test.OnUserInput += new EventTest.UserInput(HandlerMore);
// 注销事件或取消订阅
// test.OnUserInput -= new EventTest.UserInput(Handler);
}

public void Handler(object sender, EventArgs e)
{
Console.WriteLine("数据输入结束!");
}

public void HandlerMore(object sender, EventArgs e)
{
Console.WriteLine("真的结束了!");
}

static void Main(string[] args)
{
EventTest test = new EventTest();
Program program = new Program(test);
test.Method();
}
}
}

在Program的构造函数中订阅了事件,在此处,通过委托将两个方法添加到事件中,即该事件有两个订阅者,当事件触发时同时触发这些方法的执行。Program类充当了具体观察者角色,可以对目标类的事件做出响应,在此,方法Handler()和HandlerMore()即为响应方法。编译并运行程序,输出结果如下:

1
2
3
4
5
6
请输入数字:  
3
2
1
数据输入结束!
真的结束了!

如果在另一个类中也需要处理该事件,无须修改EventTest类的源代码,只需要按照委托的规范编写事件处理程序并订阅事件即可,系统具有很好的扩展性,相同的目标可以对应于不同的观察者,相同的事件可以对应于不同的事件处理程序。.NET中的事件处理模型是观察者模式的一种变形,它与观察者模式的实现原理本质上是一致的。

观察者模式与MVC

在当前流行的MVC(Model-View-Controller)架构中也应用了观察者模式,MVC是一种架构模式,它包含3个角色:模型(Model)、视图(View)和控制器(Controller)。其中,模型可对应于观察者模式中的观察目标,而视图对应于观察者,控制器可充当两者之间的中介者。当模型层的数据发生改变时,视图层将自动改变其显示内容。MVC结构示意图如图5所示。

图5 MVC结构示意图

在图5中,模型层提供的数据是视图层所观察的对象,在视图层中包含两个用于显示数据的图表对象,一个是柱状图,一个是饼状图,相同的数据拥有不同的图表显示方式,如果模型层的数据发生改变,两个图表对象将随之发生变化,这意味着图表对象依赖模型层提供的数据对象,因此数据对象的任何状态改变都应立即通知它们。同时,这两个图表之间相互独立,不存在任何联系,而且图表对象的个数没有任何限制,用户可以根据需要再增加新的图表对象,例如折线图。在增加新的图表对象时,无须修改原有类库,符合开闭原则。

观察者模式的优缺点与适用环境

观察者模式是一种使用频率非常高的设计模式,无论是移动应用、Web应用还是桌面应用,观察者模式几乎无处不在,它为实现对象之间的联动提供了一套完整的解决方案,凡是涉及一对一或者一对多的对象交互场景都可以使用观察者模式。观察者模式广泛应用于各种编程语言的GUI事件处理的实现,在基于事件的XML解析技术以及Web事件处理中也都使用了观察者模式。

观察者模式的优点

观察者模式的主要优点如下:

  • (1)观察者模式可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色。
  • (2)在观察目标和观察者之间建立一个抽象的耦合,观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。
  • (3)观察者模式支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。
  • (4)观察者模式符合开闭原则,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下增加新的观察目标也很方便。

观察者模式的缺点

观察者模式的主要缺点如下:

  • (1)如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。
  • (2)如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们进行循环调用,可能导致系统崩溃。
  • (3)观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而只是知道观察目标发生了变化。

观察者模式的适用环境

在以下情况下可以考虑使用观察者模式:

  • (1)一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。
  • (2)一个对象的改变将导致一个或多个其他对象发生改变,且不知道具体有多少对象将发生改变,也不知道这些对象是谁。
  • (3)需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……可以使用观察者模式创建一种链式触发机制。

本章小结

(1)在观察者模式中,定义了对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象都得到通知并被自动更新。观察者模式是一种对象行为型模式。

(2)观察者模式包含目标、具体目标、观察者和具体观察者4个角色。其中,目标是指被观察的对象;具体目标是目标类的子类,通常包含经常发生改变的数据,当它的状态发生改变时,将向它的各个观察者发出通知;观察者将对观察目标的改变做出反应;具体观察者是观察者的子类,实现在观察者中声明的更新数据的方法。

(3)观察者模式的主要优点是可以实现表示层和数据逻辑层的分离;在观察目标和观察者之间建立一个抽象的耦合;支持广播通信且符合开闭原则。其主要缺点是将所有的观察者都通知到会花费很多时间;如果存在循环调用可能导致系统崩溃;没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而只是知道观察目标发生了变化。

(4)观察者模式适用的环境:一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用;一个对象的改变将导致一个或多个其他对象也发生改变,且并不知道具体有多少对象将发生改变,也不知道这些对象是谁;需要在系统中创建一个触发链。

(5).NET中的委托事件模型是观察者模式在.NET中的经典应用。事件源对象充当观察目标角色,事件处理对象充当具体观察者角色,如果事件源对象的某个事件触发,则调用事件处理对象中的事件处理程序对事件进行处理。

(6)MVC架构中应用了观察者模式,其中模型对应于观察者模式中的观察目标,视图对应于观察者,控制器可充当两者之间的中介者。

0%