思考并回答以下问题:
本章涵盖:
- 说明
- 经典回顾
- .NET内置的Observer机制——事件
- 具有Observer的集合类型
- 面向服务接口的Observer
- 小结
说明
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.. —Design Patterns: Elements of Reusable Object-Oriented Software
观察者模式主要用于1:N的通知发布机制,它希望解决一个对象状态变化时可以及时告知相关依赖对象的问题,令它们也可以做出响应。生活中类似的案例很多,大至自上而下的政策调整、奥运赛场信息的发布,小至项目中一个需求点的调整,我们都可以发现存在类似的通知要求,而且无论是会议、人力还是工具,其中都存在一个角色,它的作用就是保持对相关问题(或称之为主题)的关注,在问题发生变化的时候是“TA”把消息通知相关各方。观察者模式采用的就是类似的方法,它抽象一类对象(观察者)专门负责“盯着”目标对象,当目标对象状态有变动的时候,每个观察者就会获得通知并做出响应。观察者模式解决的也是调用通知关系带来的依赖。
用一段代码作为示例也许更为直观,这里假设类型X状态更新后要同时告知A、B、C三个类型,那么一个直观的实现方式如下:
1 | /// <summary> |
UnitTest
1 | [ ] |
这里类型X只需引用A、B、C三个类型即可,但这样会给类型X带来似乎过多的依赖关系,因为它需要同时引用三个类型。不难发现,其实A、B、C三个类型本身都有类似的执行更新的方法,如果对它们进行抽象就可以让X仅仅依赖于一个抽象类型,于是对代码进行如下修改: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/// <summary>
/// 对类型A/B/C做抽象后的接口
/// </summary>
public interface IUpdatableObject
{
int Data { get; }
void Update(int newData);
}
/// <summary>
/// 具体的待更新类型
/// </summary>
public class A : IUpdatableObject
{
private int data;
public int Data{get { return this.data;}}
public void Update(int newData) { this.data = newData; }
}
public class B : IUpdatableObject
{
private int count;
public int Data { get { return this.count; } }
public void Update(int newData) { this.count = newData; }
}
public class C : IUpdatableObject
{
private int n;
public int Data { get { return this.n; } }
public void Update(int newData) { this.n = newData; }
}
public class X
{
private IUpdatableObject[] objects = new IUpdatableObject[3];
// 索引器
public IUpdatableObject this[int index] { set { objects[index] = value; } }
private int data;
public void Update(int newData)
{
this.data = newData;
foreach (IUpdatableObject obj in objects)
obj.Update(newData);
}
}
UnitTest
1 | [ ] |
这样做有什么区别呢?我们会发现如果后面示例中A\B\C实例可以通过其他方式注入,客户程序仅需要依赖一个抽象的接口——IUpdatableObject。变化后的结构如图1所示。
图1 抽象前后的1:N通知机制
通过一个简单的抽象,客户程序所依赖的类型变成一个,这样为其带来进一步扩展的灵活性,而且不容易因为A\B\C的变化受到影响。
经典回顾
上面的分析还有一个问题没有说明,即对目标对象的状态修改的抽象,不仅存在修改某个对象需要其他几个对象的情况,而且很多对象都存在类似的情况。根据我们面向对象的经验,这时候非常有必要对目标对象本身也进行一次抽象,这样任何符合类似特征的对象都可以采用类似的方式做到松散耦合的通知。观察者模式的静态结构如图2所示。
图2 经典观察者模式的静态结构
该模式不仅对目标对象进行了抽象,同时对它保持关注的对象(观察者)也进行了抽象,通过在目标对象维护一个观察者对象列表的方式,当状态变更的时候进行逐个通知。它的主要应用情景如下:
当存在一类对象通知关系上依赖于另一类对象的时候,把它们进行抽象,确保两类对象的具体实现都可以相对独立的变化,但它们交互的接口保持稳定。
一个类型状态变化时,需要通知未知数量的其他对象,例如:我们第一个例子中X明确知道有A\B\C各一个实例需要通知,但如果增加D类型或有两个A需要通知,X就需要频繁修改了。
最重要的是提供了目标对象与希望获得通知的对象间松散耦合。
下面我们看一个示例,其代码如下:
C#抽象定义部分
1 | /// <summary> |
C#具体类型部分
1 | /// <summary> |
UnitTest
1 | [ ] |
我们分析一下这个例子:
- SubjectBase本身不知道会有哪些1Observer类型希望获得它的更新通知。
- 具体的Observer类型也并不需要真正了解目标类型是什么,知道它们是SubjectcBase即可。
- SubjectBase通过一个观察者列表逐个通知,单元测试验证其有效。
- 同样,每一个观察者由于仅依赖于抽象的目标类型,因此一个观察者实际上可以跟踪多个目标类型。
下面我们讨论一下观察者如何更新自己数据,当目标对象状态更新的时候,观察者可以通过以下两种方式更新信息:
- 目标对象在通知里把需要更新的信息作为参数提供给IObserver的Update()方法。
- 目标对象仅仅告诉观察者有新的状态,至于该状态是什么,观察者自己访问目标对象来获取。
前者我们称之为“推”方式,更新的数据是目标对象硬塞给观察者的;后者被称为“拉”模式,是观察者主动从目标对象拽下来的。从面向对象来看,它们的优劣如下:
- “推”方式每次都会把通知以广播方式发送给所有观察者,所有观察者只能被动接受,如果通知的内容比较多,多个观察者同时接受可能会对网络、内存(可能还会涉及IO)有比较大的影响。
- “拉”方式更加自由,它只要知道“有情况”就可以了,至于什么时候获取、获取哪些内容、甚至是否要获取都可以自主决定,但这也带来两个问题,如果某个观察者“不紧不慢”,它可能会漏掉之前通知的内容;另外它必须保存一个对目标对象的引用,而且需要了解目标对象的结构,即产生了一定依赖。
项目中将这两种方式区分开来,其更主要原因来自于安全性要求,原则上我们都希望高信任区域可以读写低信任区域,而低信任区域不能写高信任区域,很多时候连读都不允许,这时候“推”模式比较适合高信任区域不信任低信任区域的写方式,而“拉”一般要求高信任区域信任某个低信任区域的访问,或者就是高信任区域访问低信任区域。
我们上面的示例其实是综合两者优缺点后一个折中的办法,整体上看比较贴近“推”方式,但推送的内容并不是实际的状态,而是一个对抽象目标对象的引用,观察者可以根据需要通过这个引用访问到状态,从这个角度看又是“拉”方式(见图3)。
图3 观察者模式的时序
.NET内置的Observer机制——事件
前面的章节里我们已经多次介绍过委托和事件,从其执行效果看,.NET的事件处理过程本身就是个观察者模式的范例,前面讨论过,虽然从直接的编码角度看,.NET CLR对上层众多事件的调度属于中介者方式,但从应用层的使用看事件所定义的委托类型本身就是个抽象的观察者,相对于上面示例中的观察者接口而言,它令应用结构更加松散耦合,实际上直接依赖的除了作为目标对象的EventArgs以外就是.NET CLR自己了。
上面是一个简单的.NET事件,其中观察者是满足.NET事件委托要求(例如NameChanged)所定义的具体方法,相对于我们常说的抽象观察者接口和具体观察者对象而言,.NET的委托机制更为简单,从编码上看就是抽象一个方法签名。
不难看出这个结构更加松散,客户程序不必实现一个IObserver接口,.NET事件机制是:NET内置的,观察者与目标对象之间也是松散耦合的,观察者可以随时通过重载的+=、-=的建立与目标对象的联系,同时所有的目标类型都是继承自EventArgs的,它是通用的。对于使用普通委托的情况而言,每个delegate定义其实可视为使用MulticastDelegate做组播,它可以满足我们对更多委托参数(即更多主题内容)的要求。
因此,我们建议:除非类型参数或配置体系有专门的要求,必须要定义自己的10bserver接口,项目中可以考虑用松散的委托或事件方式设计观察者。甚至我们在设计应用的时候,尤其是对应用交互部分可以采用事件驱动(EDA: Event-Driven Architect)方式,业务实体对象也好,业务流程对象也好,把对外部应用的切入点设计为各种事件,这样它们执行中可以按照自己的方式处理,至于会有多少人“盯着”其行为,操作本身并不理会。
具有Observer的集合类型
工程中很多时候我们会使用集合类型(例如: IList
.NET Framework提供的常用集合类型-Dictionary
其代码如下:
面向服务接口的Observer
在Web Service世界中同样可以借助事件机制完成两个服务对象间的通知-观察方式,比较主要的标准是微软联合相关厂商推出的WS-Eventing,不过它采用的是观察者模式的一个更进一步的变体-出版/预定模式,出版/预定模式我们会在后面的架构模式部分作介绍,本章先提一下服务接口环境下实现的概念步骤。
这很大程度上与服务调用中,发出通知的源对象难于管理需要通知的各个对象有关,采用下面的直接方式,源对象至少需要了解如下内容:
每个目标对象的地址(对于Web Service而言,大部分是URL)
如何与每个目标对象绑定,双方的通过什么协议、接口关联在一起。
服务环境下的通知关系如图4所示。
图4服务环境下的通知关系示意图
由于服务源服务接口需要服务的对象还不确定的内容非常多,也可能非常杂,如果让它实现服务的同时又去管理这些“杂事”,对其运行有不利的影响,而且承担的职责太多,同时会产生过多的对外依赖关系。虽然通过WS-MEX (Web Service MetadataExchange)可以让源与每个目标对象间完成对消息交换方式的协商,但还是免不了1:N的依赖关系。还有一个问题,即我们设计的服务接口往往不像之前示例中那么简单,需要通知的事情(正常情况、异常情况、延期超时、追加通知,同一个业务领域下的不同主体 )也比较多,相应的目标方关注的内容也不一样(例如,虽然银行提供了催缴电话费的短信服务,但这些服务不能每次都“群”,还是要有的放矢)。
所以,很有必要安排一个观察者负责关注源的动向,另外还需要增加一个持久化机制,把每个目标方关注的内容和源实际可能通知的事件类别进行登记并匹配,这样即便有各种类型、各种适用个性化的通知,也可以“按需”完成通知。在Ws-Eventing中规定了几个
概念
Event Source:实际发生事件的源。
Event Message:通知本身作为事件的消息,在松散的服务接口间传递。
Event Sink:实际接受Event Message的目标方,类似WCF中的概念体系,它也是用ABC(Address. Binding, Contract)描述的(ABC的组合常被称为ServiceEndpoint)下面我们看一下一般设计服务接口层次ws-Eventing系统的结构,如图5所示。
图5 WS-Eventing的系统结构
这里SubscriptionStore是个抽象的概念预订信息访问对象,它保存的信息包括哪些Eventsink会对何种事件感兴趣,需要接收该事件,而通知管理机制在收到新的通知事件的时候,往往采用wS-Transfer方式提取、查找相关信息。至于具体用什么技术实现WS-Transfer,则没有明确要求,比如用数据访问、用XPath查找一个xML文件都可以。该方式的时序如图6所示。
考虑到NotificationManager主要负责有关运行过程中的执行,为了和SubscriptionStore打交道,之前还需要有个对象管理对预定信息的收集、登记、撤销,所以我们在上面系统结构上增加一个SubscriptionManager的对象,它和NotificaionManager没有直接的交互,两者仅基于SubscriptionStore实现各自独立的处理,如图7所示。
这里有意把SubscriberManager的回复写在下一个操作之前,其意在说明Subscriber的预定过程可以是异步的,也可以同步执行。
小结
本章介绍了观察者模式,由于它可以比较有效地降低调用关系中1:N的依赖,同时对无法预知数量的观察者提供支持,所以无论在.NET Framework还是在项目实施中都被普遍使用。
但与此同时,观察者模式也会产生一些不太好的后果,例如:
降低了性能:不使用该模式的时候对象间是直接引用的调用过程,而且引用关系在编译阶段就决定的。
内存泄漏:即便每个主题对象上的所有观察者都已经失效了,但它们没有调用Detach方法,因此无论是观察者本身还是主题对象间都可能因为相互的引用关系无法被GC回收。
测试和调试更加困难:采用观察者模式会令调用关系的依赖不如之前的直接调用明显,因此在测试或调试的时候我们需要时刻注意每一个Attach和Detach过程,而且还要注意Notify当前遍历的位置,否则难于跟踪并找到当前实际执行的类型。
除非特殊需要,很多时候.NET环境下开发并非必须定义独立的IObserver接口,我们可以直接采用.NET的事件机制完成。