观察者模式
一、引出模式
在生活中,观察者模式非常的常见,比如到邮局、报社订阅报纸,比如QQ邮箱订阅,在比如你玩微博,关注了某“大婶”,“大婶”发布消息时,你也会相应的收到信息。
在软件开发中,就是这么一种情况。当一个对象的状态发生改变时,如何让依赖于它的所有对象得到通知,并进行相应的处理?
二、认识模式
1.模式定义
定义对象间一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
关键词:一对多,状态改变,得到通知,自动更新
2.解决思路
我们来看下订阅报纸的例子。对于报社来说,它开始是不知道有多少个订阅者来订阅报纸,因此,报社是需要维护一个订阅者的列表,这样,当新一期报纸出版时,就可以将报纸顺利地发到订阅者手里。对于订阅者来说,一份报纸应该会有多个订阅者进行订阅。
这样就出现了典型的一对多的对象关系,一份报纸对象,对应着多个订阅者对象,当报纸对象改变时,是需要通知多个订阅者的。那么怎么来维护它们之间的关系呢?
观察者模式正好可以处理这样一种情况。观察者模式把多个订阅者称为观察者(Observee),多个观察者观察的对象称为目标:(Subject)。
3.模式结构
Subject:目标对象,通常具有如下功能:
- 一个目标可以被多个观察者观察。
- 目标提供对观察者的注册与退订的维护。
- 当目标状态发生变化时,目标负责通知所有注册的、有效的观察者。
Observer:定义观察者的接口,提供目标通知时对应的更新方法,这个更新方法会进行相应的业务处理,可以在这个方法里面回调目标对象,以获取目标对象的数据。
ConcreteSubject:具体的目标实现对象,用来维护目标状态,当目标对象的状态发生变化时,通知所有的注册的、有效的观察者,让观察者进行相应的业务处理。
ConcreteObserver:观察者的具体实现对象,用来接收目标的通知,并进行相应的后续处理。
4.模式示例代码
class Program { static void Main(string[] args) { } } /// <summary> /// 观察者接口,定义一个更新的接口给那些在目标发生改变的时候被通知的对象 /// </summary> public interface Observer { /// <summary> /// 更新的接口 /// </summary> /// <param name="subject">传入目标对象,好获取相应的目标对象的状态</param> void Update(Subject subject); } /// <summary> /// 目标对象,它知道观察它的观察者,并提供注册和删除观察者的接口 /// </summary> public abstract class Subject { /// <summary> /// 用来保存注册的观察者对象 /// </summary> private List<Observer> observerList = new List<Observer>(); /// <summary> /// 注册观察者对象 /// </summary> /// <param name="observer">观察者对象</param> public void Attach(Observer observer) { observerList.Add(observer); } /// <summary> /// 删除观察者对象 /// </summary> /// <param name="observer">观察者对象</param> public void Detach(Observer observer) { observerList.Remove(observer); } /// <summary> /// 通知所有注册的观察者对象 /// </summary> public void NoyifyObserver() { foreach (var observer in observerList) { observer.Update(this); } } } /// <summary> /// 具体观察者对象,实现更新的方法,使自身的状态和目标的状态保持一致 /// </summary> public class ConcreteObserver:Observer { /// <summary> /// 示意,观者者的状态 /// </summary> private string subjectSate; /// <summary> /// 具体的更新实现 /// </summary> /// <param name="subject"></param> public void Update(Subject subject) { //这里可能需要更新观察者的状态,使其与目标的状态保持一致 subjectSate = ((ConcreteSubject) subject).GetSubject(); } } /// <summary> /// 具体的目标对象,负责把有关状态存入到相应的观察者对象,并在自己状态发生改变时,通知各个观察者 /// </summary> public class ConcreteSubject : Subject { /// <summary> /// 示意,目标对象的状态 /// </summary> private string subjectState; /// <summary> /// 获取目标对象的状态 /// </summary> /// <returns></returns> public string GetSubject() { return subjectState; } public void SetSubjectSate(string subjectState) { this.subjectState = subjectState; //状态发生了改变,通知各个观察者 base.NoyifyObserver(); } }
5.模式订阅报纸实例代码
class Program { static void Main(string[] args) { //创建一个报纸,作为被观察者 NewsPaper news=new NewsPaper(); //创建阅读者,也就是观察者 Reader reader1=new Reader(); reader1.Name = "张三"; Reader reader2=new Reader(); reader2.Name = "李四"; Reader reader3=new Reader(); reader3.Name = "王五"; //注册阅读者 news.Attach(reader1); news.Attach(reader2); news.Attach(reader3); //要出报纸啦,即状态改变了 news.SetContent("观察者模式"); Console.ReadKey(); } } /// <summary> /// 观察者,比如报纸的读者 /// </summary> public interface Observer { void Update(Subject subject); } /// <summary> /// 目标对象,作为被观察者 /// </summary> public abstract class Subject { /// <summary> /// 用来保存注册的观察者对象,也就是报纸的订阅者 /// </summary> private List<Observer> observers=new List<Observer>(); /// <summary> /// 报纸的读者需要先向报社订阅,先要注册 /// </summary> /// <param name="observer">报纸的读者 </param> public void Attach(Observer observer) { observers.Add(observer); } /// <summary> /// 报纸的读者可以取消订阅 /// </summary> /// <param name="observer">报纸的读者</param> public void Detach(Observer observer) { observers.Remove(observer); } /// <summary> /// 当每期报纸印刷出来后,就要迅速的主动的被送到读者的手中,相当于通知读者,让他们知道 /// </summary> public void NotifyObserver() { foreach (var observer in observers) { observer.Update(this); } } } /// <summary> /// 报纸对象,具体的目标实现 /// </summary> public class NewsPaper : Subject { /// <summary> /// 报纸的具体内容 /// </summary> public string Content { get; private set; } public void SetContent(string content) { Content = content; Console.WriteLine("报社出版报纸啦!本期的主题是:" + Content); //内容有了,说明又出报纸了,那就通知所有的读者 base.NotifyObserver(); } } /// <summary> /// 真正的读者,为了简单就描述一下姓名 /// </summary> public class Reader:Observer { /// <summary> /// 读者的姓名 /// </summary> public string Name { get; set; } public void Update(Subject subject) { //这是采用拉的方式 Console.WriteLine(Name+"收到报纸了,本期的主题是:"+((NewsPaper)subject).Content); } }
三、理解模式
1.目标和观察者之间的关系
按照模式的定义,目标和观察者之间是典型的一对多关系。
但是,不是就是说观察者就要多个,观察者只有一个也是可以的,只是变成了一对一关系。
同样的,一个观察者是可以观察多个目标的,如果这样的话,就要注意这个更新通知来自于哪个目标对象。
一般情况下,观察者应该为不同的目标定义不同的回调方法,这样实现最简单,不需要在update方法内部进行区分。
2.单向依赖
在观察者模式中,观察者和目标是担心依赖的,只有观察者依赖于目标对象,而目标对象是不依赖与观察者的。
它们之间联系的主动权是掌握在目标对象手里的,只有目标知道什么时候需要通知观察者。在整个过程中,观察者始终是被动的,被动的等待目标的通知,等待目标对象传值给它。
3.基本的实现说明
- l具体的目标实现对象要能维护观察者者的注册信息,最简单的方法莫过于如前面例子那样,采用一个集合来保存观察者的注册信息。
- l具体的目标对象需要维护一起通知的状态,一般是目标的自身状态。
- l具体的观察者实现对象需要能接收目标的通知,能够接收目标传递的数据,或主动去获取目标的数据,并进行后续处理。
- l如果一个观察者观察多个目标,那么在观察者的更新方法里面,需要去判断是来之哪个目标的通知。一种方案是在Update方法,通过参数来区分,另一种简单方案就是直接定义不同的回调方法。
4.推模型和拉模型
- l推模型
目标对象主动向观察者推送目标的详细信息,不管观察者需不需要,推送的信息通常是目标对象的全部或部分数据,相当与广播通信。
- l拉模型
目标对象在通知观察者的时候,值传递少量信息。如果观察者需要更具体的信息,有观察者主动到目标对象中获取,相当于是观察者从目标对象中拉数据。一般这种模型的实现中,会把目标对象自身通过Update方法传递给观察者,这样,观察者需要数据时,就可以通过这个引用获取。上面订阅报纸的例子就是一个拉模型。
5.观察者模式的优点
- 观察者模式实现了观察者和目标之间的抽象耦合。
原本目标对在状态改变时,需要直接调用所有的观察者对象,但是,抽象出观察者接口之后,目标和观察者只是抽象层面上的耦合。也就是说,目标只知道观察者的接口,而不是到具体的观察者。
- 观察者模式实现了动态联动。
由于观察者模式对观察者注册实现管理,那就可以在运行期间,通过动态控制注册的观察者,来控制某个动作的联动范围。
- 观察者模式支持广播通信。
6.何时选用观察者模式
- 当一个抽象模型有两个方面,其中一个方面的操作依赖与另一方面的状态变化就可以使用观察者模式。
- 如果在更改一个状态的时候,需要同事连带改变其他对象,而且不知道有多少个对象需要被连带改变时,可以选用观察者模式。
- 当一个对象必须通知其它对象,但你又希望这个对象和其它被它通知的对象是松散耦合的,也可以使用观察者模式。
7.观察者模式的本质
观察者模式的本质就是:触发联动。
当修改目标对象的状态时,就会触发相应的通知,然后会循环调用所有注册的观察者对象的相应放哪广发,其实就是相当于联动调用这些观察者的方法。