摘要

在今天的设计模式系列中我给大家带来了观察者模式,首先我会以一个生动的故事引入观察者模式的应用的场景,然后描述这个场景中出现的问题,最后我们提出观察者模式的解决方案,并给出C#语言实现的代码,最后以.net里的委托-事件方式的观察者模式作为结尾。

故事

小雪是一个非常漂亮的女孩,漂亮的女孩总是有很多的追求者,而且追求者的队伍在不断的变动,随时有人进入这个队伍,也有人退出。男孩们追求女孩时总是表现 出120%的关心,当小雪私自游玩时总是不断收到追求者询问小雪位置变动的消息,小雪也不胜其烦,但小雪是如此的一个善良的女孩,她总是打断自己正常的生 活回复男孩们的消息。而男孩们由于要不断的关心小雪的位置变化也弄的精疲力竭,而且还影响正常的工作。

在这样一个简单的故事场景中我们发现了什么?来看看小雪和男孩们的烦恼:

男孩们必须不断的询问小雪的位置变化,从而打断正常的工作
小雪也要不断的接受男孩们的询问,有的时候小雪的位置并没有发生变化,还是要不断的回复男孩们的询问,也影响正常的工作。
如果给各个男孩们回复问题的方式都不尽相同,小雪还要知道不同的回复方式,而且不断的有新的男孩们增加进来,还不知道未来有什么新的回复方式。

看到这么多烦恼,我们创意无限的Nokia公司给小雪和男孩们提出了解决方案:
Nokia公司荣誉出品了一款带有GPRS功能的手机,该手机保存着一个订阅位置变化短信通知的电话列表,当该手机检测到位置发生变化就会向这个订阅列表里的所有手机发送短信。看到Nokia这个解决方案,男孩们和小雪都应该松一口气,他们各自都可以按照自己正常的生活习惯,只有状态发生变化时候各自才会进行通信

观察者模式的解决方案

在上面Nokia的解决方案中就透露出观察者模式的思想:观察者模式定义了对象之间一对多的依赖,当这个对象的状态发生改变的时候,多个对象会接受到通知,有机会做出反馈。在运行的时刻可以动态的添加和删除观察者。

带着这个定义我们来看看尝试实现上面的观察者模式

首先在观察者模式中我们必须定义一个所有“观察者”都必须实现的接口,

这样被观察者向观察者发送消息的时候就可以使用统一的方式,这也符合面相对象原则中的面向接口编程:

//所有观察者都必须实现
public interface IBoy
{
//向男孩们显示小雪位置情况,也就是向观察者发送消息,观察者还可以对此做出反馈
void Show(string address);
}
using System;
//男孩A,一个观察者
public class BoyA : IBoy
{
public void Show(string address)
{
//假设经过处理后为韩文的地址
Console.WriteLine("A:"+address);
}
} 

using System;
//男孩B,又一个观察者
public class BoyB : IBoy
{
public void Show(string address)
{
//假设经过处理后为英语的地址
Console.WriteLine("B:"+address);
}
} 

下面看看小雪的实现,也就是被观察者,
主要看看那个订阅的电话列表和怎样将消息通知给观察者.
using System;
using System.Collections.Generic; 

public class GPRSMobile
{
//保存一个观察者列表
private List<IBoy> boys = null;
private string address = "";
public GPRSMobile()
{
boys = new List<IBoy>();
}
//添加观察者
public void AddBoy(IBoy b)
{
boys.Add(b);
}
public void RemoveBoy(IBoy b)
{
boys.Remove(b);
}
//通知
private void Notify(string address)
{
for (int i = 0; i < boys.Count; i++)
{
boys[i].Show(address);
}
}
//位置发生变化
public void OnAddressChanaged(string newAddress)
{
//假设这里的地址是中文形式的
Notify(newAddress);
}
}

看到上面的代码例子,我们可以给观察者模式的实现总结出这样几条规律:
第一,被观察者必须保存着一个观察者列表。
第二,所有的观察者必须实现一个统一的接口。

那观察者模式到底有哪些好处呢?在上面的例子中我们可以看到被观察者仅仅
依赖于一个实现了观察者接口的列表,我们可以随时的向这个列表添加观察者,
而被观 察者无须关注观察者是如何实现的。当我们向观察者族中添加一个新的观察者,
被观察者无须作任何改变,新的类型只要实现了观察者接口即可。
在上面的描述中我们仿佛看到了面向接口编程无穷的力量,
面向接口编程使实现依赖于接口,也就是实现依赖于抽象。

这样在被依赖对象发生改变的时候,只要接口没有发生变化时,

依赖对象无须作任何变动。

在现实中存在着很多观察者模式的实例。比如在这个全民炒股的时代,
每个持有股票的人总是不断的关注着自己所买的股票的走势,
有人天天呆在交易大厅里看着屏幕上股票价格的走势,
有人在工作时间盯着电脑里股票软件
为此很多公司采取各种各样的政策来制止这种行为,这样不仅影响正常的上班,
且股票交易大厅常常人满为患。如果有这样一个服务,只要你订阅一个短信,
这个服务就会在你所关注的股票价格发生变动的时候短信通知你,
这样你就可以按照正常的顺序来做你的工作。

.net中的观察者模式

在.net中,微软给我们带来一个更好的观察者模式的实现:事件-委托.

在Gof的观察者模式中(姑且称之为经典设计模式吧),观察者必须实现一个统一的接口,
在.net里这个接口由委托的签名来保证了,.net里的委托就是一个安全
函数指针(之所以说安全是与以前的C指针相比的,
C的函数指针并不包括函数的签名比如参数等东西,
所以可以传递一个并不是你期望的函数进去,导致运行时出 错,
由于这种错误在运行时发生,很难检查出来)。
Ok,现在以一个.net的委托-事件的例子结束今天的观察者模式吧。

描述:这是一个控制台程序,程序接收一个0到100之间整型的输入,
程序接收到输入后开始一个从0到100的循环,当循环到你输入的数字的时候做一些处理,
我们将以两种方式来描述这个实例,先用常规的方式,然后采用委托-事件的方式

public class Program
{
static void Main(string[] args)
{
Console.WriteLine("Please Input a 0-100 Number:");
int input = Console.Read();
if (input < 0 || input > 100)
{
Console.WriteLine("Error");
}
for (int i = 0; i <= 100; i++)
{
if (i == input)
{
//屏幕输出
Console.WriteLine(i.ToString());
//弹出提示框
MessageBox.Show(i.ToString());
//可能还有其他处理
}
}
}
}

看到这个例子有什么感觉?循环的代码和处理的代码混在一起,
可能还有未知的处理方式添加进来。耦合度非常高。再看看.net的处理方式吧

namespace Observer
{
//定义一个委托,这里定义了观察者方法的签名,就是一个协议吧
public delegate void NumberEventHandler(object sender,NumberEventArgs e);
//要传递哪些参数到观察者?在这里定义,注意,要继承自EventArgs
public class NumberEventArgs : EventArgs
{
public NumberEventArgs(int number)
{
_number = number;
}
private int _number; 

public int Number
{
get { return _number;}
set { _number = value;}
} 

}
//观察者模式中的主题
public class Subject
{
//定义一个事件,就是委托的实例了
public event NumberEventHandler NumberReached; 

public void DoWithLoop(int number)
{
for (int i = 0; i <= 100; i++)
{
//触发事件的条件到了
if (i == number)
{
NumberEventArgs e = new NumberEventArgs(i);
OnNumberReached(e);
}
}
}
//注意,这个方法定义为保护的,虚拟的,代表子类还可以进行覆盖,
改变触发事件的行为
//甚至可以不触发事件
protected virtual void OnNumberReached(NumberEventArgs e)
{
//判断事件是否为null,也就是是否绑定了方法
if (NumberReached != null)
NumberReached(this, e);
}
}
public class MainProgram
{
public static void Main()
{
Console.WriteLine("Please Input a 0-100 Number:");
int input = Console.Read();
if (input < 0 || input > 100)
{
Console.WriteLine("Error");
} 

Subject s = new Subject();
//给事件绑定方法,静态的
s.NumberReached += new NumberEventHandler(msgbox_NumberReached);
MainProgram mp = new MainProgram();
//给事件绑定方法,实例方法
s.NumberReached += new NumberEventHandler(mp.console_NumberReached);
s.DoWithLoop(input);
Console.Read();
}
void console_NumberReached(object sender, NumberEventArgs e)
{
Console.WriteLine(e.Number.ToString());
}
static void msgbox_NumberReached(object sender, NumberEventArgs e)
{
MessageBox.Show(e.Number.ToString());
}
}
}

虽然这个例子代码多多了,但是是值得的,事件触发的地方和处理的地方完全分离了,
循环的位置不再需要知道有多少个方法正等着处理它

总结

经过几篇设计模式文章的介绍,也许有人会觉得设计模式一直在尝试解决几个问题:解藕,封装变化。设计模式一直在为可维护性,可扩展性,灵活性努力着。所以 学习设计模式并不是了解设计模式的原型,重要的是了解设计模式的场景和目的,这样你也可以在你自己的工作中总结出自己的设计模式。
有人说中国的数学教育是个错误,学习数学并不是学习那些定理公式,学习那些东西永远是跟在别人的后面,学习数学应该注重数学史的学习,循着数学发展的历史,了解前人是怎样分析问题,解决问题,学习前人的“渔”,并不仅仅是为了得到“鱼”。

本来上面的文章已经写定了,但今天看一MVP的文章又有点新的感触,
觉得上面的总结又有点偏颇,学习模式重要的是她的精髓,
但是“作为初学者即使知道所有 设计原则但是却不知道如何在项目应用”。
是的,也许学习设计模式也要从“量变”引起“质变”。
大量的应用,先不管是否是过度设计,到一定的时候也许就会得 到思想上的升华。