概述

在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。

意图

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。[GOF 《设计模式》]

结构图

图1 Observer模式结构图

生活中的例子

观察者定义了对象间一对多的关系,当一个对象的状态变化时,所有依赖它的对象都得到通知并且自动地更新。拍卖演示了这种模式。每个投标人都有一个标有数字的牌子用于出价。拍卖师开始拍卖时,他观察是否有牌子举起出价。每次接受一个新的出价都改变了拍卖的当前价格,并且广播给所有的投标人进行新的出价。

图2 使用拍卖例子的观察者模式

Observer模式解说

下面通过一个例子来说明Observer模式。监控某一个公司的股票价格变化,可以有多种方式,通知的对象可以是投资者,或者是发送到移动设备,还有电子邮件等。一开始我们先不考虑Observer模式,通过一步步地重构,最终重构为Observer模式。现在有这样两个类:Microsoft和Investor,如下图所示:

图3 UML静态图示例

它们的实现如下:

public class Microsoft

{
private Investor _investor; private String _symbol; private double _price; public void Update() {
_investor.SendData(this);
} public Investor Investor {
get { return _investor; } set { _investor = value; }
} public String Symbol {
get { return _symbol; } set { _symbol = value; }
} public double Price
{
get { return _price; } set { _price = value; }
}
} public class Investor {
private string _name; public Investor(string name) {
this._name = name;
} public void SendData(Microsoft ms) {
Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}", _name, ms.Symbol,ms.Price); } }

简单的客户端实现:

class Program

{

    static void Main(string[] args)

    {

        Investor investor = new Investor("Jom");

        Microsoft ms = new Microsoft();

        ms.Investor = investor;

        ms.Symbol = "Microsoft";

        ms.Price = 120.00;

        ms.Update();

        Console.ReadLine();

    }

}

运行后结果如下:

Notified Jom of Microsoft's change to ¥120

可以看到,这段代码运行并没有问题,也确实实现了我们最初的设想的功能,把Microsoft的股票价格变化通知到了Jom投资者那儿。但是这里面出现了如下几个问题:

1.Microsoft和Investor之间形成了一种双向的依赖关系,即Microsoft调用了Investor的方法,而Investor调用了Microsoft类的属性。如果有其中一个类变化,有可能会引起另一个的变化。

2.当出现一种的通知对象,比如说是移动设备Mobile:

public class Mobile

{
private string _no; public Mobile(string No) {
this._no = No;
} public void SendData(Microsoft ms) {
Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}", _no, ms.Symbol, ms.Price); } }

这时候对应的Microsoft的类就应该改变为如下代码,在Microsot类中增加Mobile,同时修改Update()方法使其可以通知到移动设备:

public class Microsoft

{
private Investor _investor; private Mobile _mobile; private String _symbol; private double _price; public void Update() {
_investor.SendData(this); _mobile.SendData(this);
} public Mobile Mobile {
get { return _mobile; } set { _mobile = value; }
} public Investor Investor {
get { return _investor; } set { _investor = value; }
} public String Symbol {
get { return _symbol; } set { _symbol = value; }
} public double Price {
get { return _price; } set { _price = value; }
} }

显然这样的设计极大的违背了“开放-封闭”原则,这不是我们所想要的,仅仅是新增加了一种通知对象,就需要对原有的Microsoft类进行修改,这样的设计是很糟糕的。对此做进一步的抽象,既然出现了多个通知对象,我们就为这些对象之间抽象出一个接口,用它来取消Microsoft和具体的通知对象之间依赖。

图4 静态UML图示例

实现代码如下:

public interface IObserver

{
void SendData(Microsoft ms);
} public class Investor : IObserver {
private string _name; public Investor(string name) {
this._name = name;
} public void SendData(Microsoft ms) {
Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}", _name, ms.Symbol,ms.Price); }
} public class Microsoft {
private IObserver _investor; private String _symbol; private double _price; public void Update() {
_investor.SendData(this);
} public String Symbol {
get { return _symbol; } set { _symbol = value; }
} public double Price {
get { return _price; } set { _price = value; }
} public IObserver Investor {
get { return _investor; } set { _investor = value; }
} }

做到这一步,可以看到,我们在降低两者的依赖性上已经迈进了一小步,正在朝着弱依赖性这个方向变化。在Microsoft类中已经不再依赖于具体的Investor,而是依赖于接口IObserver。

但同时我们看到,再新出现一个移动设备这样的通知对象,Microsoft类仍然需要改变,对此我们再做如下重构,在Microsoft中维护一个IObserver列表,同时提供相应的维护方法。

图5 静态UML示例图

Microsoft类的实现代码如下:

public class Microsoft

{
private List<IObserver> observers = new List<IObserver>(); private String _symbol; private double _price; public void Update() {
foreach (IObserver ob in observers)
{
ob.SendData(this);
} } public void AddObserver(IObserver observer) {
observers.Add(observer);
} public void RemoveObserver(IObserver observer) {
observers.Remove(observer);
} public String Symbol {
get { return _symbol; } set { _symbol = value; }
} public double Price {
get { return _price; } set { _price = value; }
} }

此时客户端的调用代码:

class Program

{
static void Main(string[] args) {
IObserver investor1 = new Investor("Jom"); IObserver investor2 = new Investor("TerryLee"); Microsoft ms = new Microsoft(); ms.Symbol = "Microsoft"; ms.Price = 120.00; ms.AddObserver(investor1); ms.AddObserver(investor2); ms.Update(); Console.ReadLine();
}
}

走到这一步,已经有了Observer模式的影子了,Microsoft类不再依赖于具体的Investor,而是依赖于抽象的IOberver。存在着的一个问题是Investor仍然依赖于具体的公司Microsoft,况且公司还会有很多IBM,Google等,解决这样的问题很简单,只需要再对Microsoft类做一次抽象。如下图所示:

图6 静态UML示例图

实现代码如下:

public abstract class Stock

{
private List<IObserver> observers = new List<IObserver>(); private String _symbol; private double _price; public Stock(String symbol, double price) {
this._symbol = symbol; this._price = price;
} public void Update() {
foreach (IObserver ob in observers) {
ob.SendData(this);
} } public void AddObserver(IObserver observer) {
observers.Add(observer);
} public void RemoveObserver(IObserver observer) {
observers.Remove(observer);
} public String Symbol {
get { return _symbol; }
} public double Price {
get { return _price; }
}
} public class Microsoft : Stock { public Microsoft(String symbol, double price) : base(symbol, price) { }
} public interface IObserver {
void SendData(Stock stock);
} public class Investor : IObserver {
private string _name; public Investor(string name) {
this._name = name;
} public void SendData(Stock stock) {
Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}", _name, stock.Symbol,stock.Price); } }

客户端程序代码如下:

class Program

{
static void Main(string[] args) {
Stock ms = new Microsoft("Microsoft",120.00); ms.AddObserver(new Investor("Jom")); ms.AddObserver(new Investor("TerryLee")); ms.Update(); Console.ReadLine();
} }

到这里我们可以看到,通过不断的重构,不断地抽象,我们由一开始的很糟糕的设计,逐渐重构为使用Observer模式的这样一个方案。在这个例子里面,IOberser充当了观察者的角色,而Stock则扮演了主题对象角色,在任何时候,只要调用了Stock的Update()方法,它就会通知它的所有观察者对象。同时可以看到,通过Observer模式,取消了直接依赖,变为间接依赖,这样大大提供了系统的可维护性和可扩展性。

推模式与拉模式

对于发布-订阅模型,大家都很容易能想到推模式与拉模式,用SQL Server做过数据库复制的朋友对这一点很清楚。在Observer模式中同样区分推模式和拉模式,我先简单的解释一下两者的区别:推模式是当有消息时,把消息信息以参数的形式传递(推)给所有观察者,而拉模式是当有消息时,通知消息的方法本身并不带任何的参数,是由观察者自己到主体对象那儿取回(拉)消息。知道了这一点,大家可能很容易发现上面我所举的例子其实是一种推模式的Observer模式。我们先看看这种模式带来了什么好处:当有消息时,所有的观察者都会直接得到全部的消息,并进行相应的处理程序,与主体对象没什么关系,两者之间的关系是一种松散耦合。但是它也有缺陷,第一是所有的观察者得到的消息是一样的,也许有些信息对某个观察者来说根本就用不上,也就是观察者不能“按需所取”;第二,当通知消息的参数有变化时,所有的观察者对象都要变化。鉴于以上问题,拉模式就应运而生了,它是由观察者自己主动去取消息,需要什么信息,就可以取什么,不会像推模式那样得到所有的消息参数。OK,说到这儿,你是否对于推模式和拉模式有了一点了解呢?我把前面的例子修改为了拉模式,供大家参考,可以看到通知方法是没有任何参数的:

public abstract class Stock

{
private List<IObserver> observers = new List<IObserver>(); private String _symbol; private double _price; public Stock(String symbol, double price) {
this._symbol = symbol; this._price = price;
} public void Update() {
foreach (IObserver ob in observers) {
ob.SendData();
}
} public void AddObserver(IObserver observer) {
observers.Add(observer);
} public void RemoveObserver(IObserver observer) {
observers.Remove(observer);
} public String Symbol {
get { return _symbol; }
} public double Price {
get { return _price; }
}
} public class Microsoft : Stock {
public Microsoft(String symbol, double price) : base(symbol, price)
{ }
} public interface IObserver {
void SendData();
} public class Investor : IObserver {
private string _name; private Stock _stock; public Investor(string name,Stock stock) {
this._name = name; this._stock = stock;
} public void SendData() {
Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}", _name, _stock.Symbol, _stock.Price); }
} class Program {
static void Main(string[] args) {
Stock ms = new Microsoft("Microsoft", 120.00); ms.AddObserver(new Investor("Jom",ms)); ms.AddObserver(new Investor("TerryLee",ms)); ms.Update(); Console.ReadLine();
} }

当然拉模式也是有一些缺点的,主体对象和观察者之间的耦合加强了,但是这可以通过抽象的手段使这种耦合关系减到最小。[感谢idior的意见]

.NET中的Observer模式

在.NET中,相信大家对于事件和委托都已经不陌生了,这里就不具体多说了。利用事件和委托来实现Observer模式我认为更加的简单和优雅,也是一种更好的解决方案。因为在上面的示例中我们可以看到,虽然取消了直接耦合,但是又引入了不必要的约束(暂且这么说吧)。即那些子类必须都继承于主题父类,还有观察者接口等。网上有很多这方面的例子,上面的例子简单的用事件和委托实现如下,仅供大家参考:

class Program
{
static void Main(string[] args)
{
Stock stock = new Stock("Microsoft", 120.00); Investor investor = new Investor("Jom"); stock.NotifyEvent += new NotifyEventHandler(investor.SendData); stock.Update(); Console.ReadLine();
}
} public delegate void NotifyEventHandler(object sender); public class Stock {
public NotifyEventHandler NotifyEvent; private String _symbol; private double _price; public Stock(String symbol, double price) {
this._symbol = symbol; this._price = price;
} public void Update() {
OnNotifyChange();
} public void OnNotifyChange() {
if (NotifyEvent != null) {
NotifyEvent(this);
} } public String Symbol {
get { return _symbol; }
} public double Price {
get { return _price; }
}
} public class Investor {
private string _name; public Investor(string name) {
this._name = name;
} public void SendData(object obj) {
if (obj is Stock) {
Stock stock = (Stock)obj; Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}", _name, stock.Symbol, stock.Price);
} } }

效果及实现要点

1.使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达到松耦合。

2.目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。观察者自己决定是否需要订阅通知。目标对象对此一无所知。

3.在C#中的Event。委托充当了抽象的Observer接口,而提供事件的对象充当了目标对象,委托是比抽象Observer接口更为松耦合的设计。

适用性

1.当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。

2.当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。

3.当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。

总结

通过Observer模式,把一对多对象之间的通知依赖关系的变得更为松散,大大地提高了程序的可维护性和可扩展性,也很好的符合了开放-封闭原则。

参考资料

Erich Gamma等,《设计模式:可复用面向对象软件的基础》,机械工业出版社

Robert C.Martin,《敏捷软件开发:原则、模式与实践》,清华大学出版社

阎宏,《Java与模式》,电子工业出版社

Alan Shalloway James R. Trott,《Design Patterns Explained》,中国电力出版社

MSDN WebCast 《C#面向对象设计模式纵横谈(19):Observer 观察者模式(行为型模式)》

NET设计模式 第二部分 行为型模式(18):观察者模式(Observer Pattern)的更多相关文章

  1. 设计模式----行为型模式之观察者模式(Observer Pattern)

    下面是阅读<Head First设计模式>的笔记. 观察者模式 定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新. JDK API内置机制 ...

  2. NET设计模式 第二部分 行为型模式(17):迭代器模式(Iterator Pattern)

    概述 在面向对象的软件设计中,我们经常会遇到一类集合对象,这类集合对象的内部结构可能有着各种各样的实现,但是归结起来,无非有两点是需要我们去关心的:一是集合内部的数据存储结构,二是遍历集合内部的数据. ...

  3. NET设计模式 第二部分 创建型模式(6):创建型模式专题总结(Creational Pattern)

    创建型模式专题总结(Creational Pattern) ——.NET设计模式系列之七 Terrylee,2006年1月 概述 创建型模式,就是用来创建对象的模式,抽象了实例化的过程.它帮助一个系统 ...

  4. .NET设计模式 第二部分 创建型模式(1)—:单件模式(Singleton Pattern)

    单件模式(Singleton Pattern) ——.NET设计模式系列之二 Terrylee,2005年12月07日 概述 Singleton模式要求一个类有且仅有一个实例,并且提供了一个全局的访问 ...

  5. NET设计模式 第二部分 行为型模式(15):模版方法模式(Template Method)

    摘要:Template Method模式是比较简单的设计模式之一,但它却是代码复用的一项基本的技术,在类库中尤其重要. 主要内容 1.概述 2.Template Method解说 3..NET中的Te ...

  6. 面向对象程序设计(OOP设计模式)-行为型模式之观察者模式的应用与实现

    课程名称:程序设计方法学 实验5:OOP设计模式-行为型模式的应用与实现 时间:2015年12月02日三,第3.4节地点:理 一.实验目的 加深对行为型设计模式的理解以及在开发中的实际应用能力. 二. ...

  7. .NET设计模式 第二部分 创建型模式(3)—建造者模式(Builder Pattern)

    建造者模式(Builder Pattern) ——.NET设计模式系列之四 Terrylee,2005年12月17日 概述 在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对 ...

  8. NET设计模式 第二部分 行为型模式(16):命令模式(Command Pattern)

    命令模式(Command Pattern) ——.NET设计模式系列之十七 TerryLee,2006年7月 概述 在软件系统中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”.但在某些场合,比 ...

  9. NET设计模式 第二部分 创建型模式(5):原型模式(Prototype Pattern)

    原型模式(Prototype Pattern) ——.NET设计模式系列之六 Terrylee,2006年1月 概述 在软件系统中,有时候面临的产品类是动态变化的,而且这个产品类具有一定的等级结构.这 ...

随机推荐

  1. java学习笔记18(基本类型包装类,system类)

    基本类型包装类 定义:程序界面用户输入的数据都是以字符串类型存储的,如果需要操作这些字符串进行运算,需要转成基本数据类型,这时就要用到基本类型包装类,例: public class Demo { pu ...

  2. angular 项目 error TS2451: Cannot redeclare block-scoped variable 'ngDevMode'

    删除  node_modules ,用 npm install 就可以了, cnpm install (竟然不行)

  3. 前端笔记 (3.JavaScript 1)

    JavaScript 是属于网络的脚本语言! JavaScript 是一种轻量级的编程语言. JavaScript 是可插入 HTML 页面的编程代码. JavaScript 插入 HTML 页面后, ...

  4. Power BI新主页将使内容的导航和发现变得轻而易举!

    微软Power BI 将在近日发布Power BI Home登陆页面的公开预览以及Power BI服务中的新全局搜索功能.登录页将成为所有内容的一站式集合,并提供更快捷的方式来分享你的仪表板.原来在左 ...

  5. pprof进行golang程序性能分析

    一.导入包 import _ "net/http/pprof" 二.启动监控routine go func() { http.ListenAndServe("0.0.0. ...

  6. php获取真实ip地址(转)

    REMOTE_ADDR只能获取访问者本地连接中设置的IP,如中南民族大学校园网中自己设置的10.X.XXX.XXX系列IP,而这个函数获取的是局域网网关出口的IP地址, 如果访问者使用代理服务器,将不 ...

  7. CCF-棋局评估 201803-04(版本 2.0)------(之前写了一个臃肿的1.0版 ,还沾沾自喜 233)

    核心 : 博弈搜索树     双方得分互为相反数 dfs (x,y,player): 玩家player下完(x,y)之后的得分最大值 易错: 先判断输赢,再判断平局 待改进: check() 函数写的 ...

  8. 初始if..else 条件语句

    if..else条件语句 如果我们希望有效的响应用户的输入,代码就需要具有判断能力.能够让程序进行判断的结构成为条件,条件判断语句返回的是布尔值真或假,真就执行一条线路,假就执行另外一条线路 if 条 ...

  9. [原]android 链接错误

    由于没有使用NDK的makefile, 而是把NDK的toolchain集成到现有的build system, 所以出现了诡异的错误: unsupported dynamic reloc R_ARM_ ...

  10. 【git】如何向gitHub上推送自己的项目

    一.在本地建立项目spring 二.在gitHub上创建spring仓库 三.在本地生成公私钥文件 命令:ssh-keygen -t rsa -C "shangxiaofei3@163.co ...