# C#设计模式(17)——观察者模式(Observer Pattern)
## 一、引言
在现实生活中,处处可见观察者模式,例如,微信中的订阅号,订阅博客和QQ微博中关注好友,这些都属于观察者模式的应用。在这一章将分享我对观察者模式的理解,废话不多说了,直接进入今天的主题。
## 二、 观察者模式的介绍
## 2.1 观察者模式的定义
从生活中的例子可以看出,只要对订阅号进行关注的客户端,如果订阅号有什么更新,就会直接推送给订阅了的用户。从中,我们就可以得出观察者模式的定义。
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己的行为。
## 2.2 观察者模式的结构
从上面观察者模式的定义和生活中的例子,很容易知道,观察者模式中首先会存在两个对象,一个是观察者对象,另一个就是主题对象,然而,根据面向接口编程的原则,则自然就有抽象主题角色和抽象观察者角色。理清楚了观察者模式中涉及的角色后,接下来就要理清他们之间的关联了,要想主题对象状态发生改变时,能通知到所有观察者角色,则自然主题角色必须所有观察者的引用,这样才能在自己状态改变时,通知到所有观察者。有了上面的分析,下面观察者的结构图也就很容易理解了。具体结构图如下所示:
![](https://box.kancloud.cn/2016-01-23_56a2eb3d83cb0.png)
图 观察者模式结构图
可以看出,在观察者模式的结构图有以下角色:
* 抽象主题角色(Subject):抽象主题把所有观察者对象的引用保存在一个列表中,并提供增加和删除观察者对象的操作,抽象主题角色又叫做抽象被观察者角色,一般由抽象类或接口实现。
* 抽象观察者角色(Observer):为所有具体观察者定义一个接口,在得到主题通知时更新自己,一般由抽象类或接口实现。
* 具体主题角色(ConcreteSubject):实现抽象主题接口,具体主题角色又叫做具体被观察者角色。
* 具体观察者角色(ConcreteObserver):实现抽象观察者角色所要求的接口,以便使自身状态与主题的状态相协调。
## 2.3 观察者模式的实现
下面以微信订阅号的例子来说明观察者模式的实现。现在要实现监控腾讯游戏订阅号的状态的变化。这里一开始不采用观察者模式来实现,而通过一步步重构的方式,最终重构为观察者模式。因为一开始拿到需求,自然想到有两个类,一个是腾讯游戏订阅号类,另一个是订阅者类。订阅号类中必须引用一个订阅者对象,这样才能在订阅号状态改变时,调用这个订阅者对象的方法来通知到订阅者对象。有了这个分析,自然实现的代码如下所示:
```
1 // 腾讯游戏订阅号类
2 public class TenxunGame
3 {
4 // 订阅者对象
5 public Subscriber Subscriber {get;set;}
6
7 public String Symbol {get; set;}
8
9 public string Info {get ;set;}
10
11 public void Update()
12 {
13 if (Subscriber != null)
14 {
15 // 调用订阅者对象来通知订阅者
16 Subscriber.ReceiveAndPrintData(this);
17 }
18 }
19
20 }
21
22 // 订阅者类
23 public class Subscriber
24 {
25 public string Name { get; set; }
26 public Subscriber(string name)
27 {
28 this.Name = name;
29 }
30
31 public void ReceiveAndPrintData(TenxunGame txGame)
32 {
33 Console.WriteLine("Notified {0} of {1}'s" + " Info is: {2}", Name, txGame.Symbol, txGame.Info);
34 }
35 }
36
37 // 客户端测试
38 class Program
39 {
40 static void Main(string[] args)
41 {
42 // 实例化订阅者和订阅号对象
43 Subscriber LearningHardSub = new Subscriber("LearningHard");
44 TenxunGame txGame = new TenxunGame();
45
46 txGame.Subscriber = LearningHardSub;
47 txGame.Symbol = "TenXun Game";
48 txGame.Info = "Have a new game published ....";
49
50 txGame.Update();
51
52 Console.ReadLine();
53 }
54 }
```
上面代码确实实现了监控订阅号的任务。但这里的实现存在下面几个问题:
* TenxunGame类和Subscriber类之间形成了一种双向依赖关系,即TenxunGame调用了Subscriber的ReceiveAndPrintData方法,而Subscriber调用了TenxunGame类的属性。这样的实现,如果有其中一个类变化将引起另一个类的改变。
* 当出现一个新的订阅者时,此时不得不修改TenxunGame代码,即添加另一个订阅者的引用和在Update方法中调用另一个订阅者的方法。
上面的设计违背了“开放——封闭”原则,显然,这不是我们想要的。**对此我们要做进一步的抽象,既然这里变化的部分是新订阅者的出现**,这样我们可以对订阅者抽象出一个接口,用它来取消TenxunGame类与具体的订阅者之间的依赖,做这样一步改进,确实可以解决TenxunGame类与具体订阅者之间的依赖,使其依赖与接口,从而形成弱引用关系,但还是不能解决出现一个订阅者不得不修改TenxunGame代码的问题。对此,我们可以做这样的思考——**订阅号存在多个订阅者,我们可以采用一个列表来保存所有的订阅者对象,在订阅号内部再添加对该列表的操作,这样不就解决了出现新订阅者的问题了嘛。并且订阅号也属于变化的部分,所以,我们可以采用相同的方式对订阅号进行抽象,抽象出一个抽象的订阅号类**,这样也就可以完美解决上面代码存在的问题了,具体的实现代码为:
```
1 // 订阅号抽象类
2 public abstract class TenXun
3 {
4 // 保存订阅者列表
5 private List<IObserver> observers = new List<IObserver>();
6
7 public string Symbol { get; set; }
8 public string Info { get; set; }
9 public TenXun(string symbol, string info)
10 {
11 this.Symbol = symbol;
12 this.Info = info;
13 }
14
15 #region 新增对订阅号列表的维护操作
16 public void AddObserver(IObserver ob)
17 {
18 observers.Add(ob);
19 }
20 public void RemoveObserver(IObserver ob)
21 {
22 observers.Remove(ob);
23 }
24 #endregion
25
26 public void Update()
27 {
28 // 遍历订阅者列表进行通知
29 foreach (IObserver ob in observers)
30 {
31 if (ob != null)
32 {
33 ob.ReceiveAndPrint(this);
34 }
35 }
36 }
37 }
38
39 // 具体订阅号类
40 public class TenXunGame : TenXun
41 {
42 public TenXunGame(string symbol, string info)
43 : base(symbol, info)
44 {
45 }
46 }
47
48 // 订阅者接口
49 public interface IObserver
50 {
51 void ReceiveAndPrint(TenXun tenxun);
52 }
53
54 // 具体的订阅者类
55 public class Subscriber : IObserver
56 {
57 public string Name { get; set; }
58 public Subscriber(string name)
59 {
60 this.Name = name;
61 }
62
63 public void ReceiveAndPrint(TenXun tenxun)
64 {
65 Console.WriteLine("Notified {0} of {1}'s" + " Info is: {2}", Name, tenxun.Symbol, tenxun.Info);
66 }
67 }
68
69 // 客户端测试
70 class Program
71 {
72 static void Main(string[] args)
73 {
74 TenXun tenXun = new TenXunGame("TenXun Game", "Have a new game published ....");
75
76 // 添加订阅者
77 tenXun.AddObserver(new Subscriber("Learning Hard"));
78 tenXun.AddObserver(new Subscriber("Tom"));
79
80 tenXun.Update();
81
82 Console.ReadLine();
83 }
84 }
```
上面代码是我们进行重构后的实现,重构后的代码实现类图如下所示:
![](https://box.kancloud.cn/2016-01-23_56a2eb3d92afe.png)
从上图可以发现,这样的实现就是观察者模式的实现。这样,在任何时候,只要调用了TenXun类的Update方法,它就会通知所有的观察者对象,同时,可以看到,观察者模式,取消了直接依赖,变为间接依赖,这样大大提供了系统的可维护性和可扩展性。这里并不是直接给出观察者模式的实现,而是通过一步步重构的方式来引出观察者模式的实现,相信通过这个方式,大家可以更深刻地理解观察者模式所解决的问题和带来的好处。
## 三、.NET 中观察者模式的应用
在.NET中,我们可以使用委托与事件来简化观察者模式的实现,上面的例子用事件和委托的实现如下代码所示:
```
1 namespace ObserverInNET
2 {
3 class Program
4 {
5 // 委托充当订阅者接口类
6 public delegate void NotifyEventHandler(object sender);
7
8 // 抽象订阅号类
9 public class TenXun
10 {
11 public NotifyEventHandler NotifyEvent;
12
13 public string Symbol { get; set; }
14 public string Info { get; set; }
15 public TenXun(string symbol, string info)
16 {
17 this.Symbol = symbol;
18 this.Info = info;
19 }
20
21 #region 新增对订阅号列表的维护操作
22 public void AddObserver(NotifyEventHandler ob)
23 {
24 NotifyEvent += ob;
25 }
26 public void RemoveObserver(NotifyEventHandler ob)
27 {
28 NotifyEvent -= ob;
29 }
30
31 #endregion
32
33 public void Update()
34 {
35 if (NotifyEvent != null)
36 {
37 NotifyEvent(this);
38 }
39 }
40 }
41
42 // 具体订阅号类
43 public class TenXunGame : TenXun
44 {
45 public TenXunGame(string symbol, string info)
46 : base(symbol, info)
47 {
48 }
49 }
50
51 // 具体订阅者类
52 public class Subscriber
53 {
54 public string Name { get; set; }
55 public Subscriber(string name)
56 {
57 this.Name = name;
58 }
59
60 public void ReceiveAndPrint(Object obj)
61 {
62 TenXun tenxun = obj as TenXun;
63
64 if (tenxun != null)
65 {
66 Console.WriteLine("Notified {0} of {1}'s" + " Info is: {2}", Name, tenxun.Symbol, tenxun.Info);
67 }
68 }
69 }
70
71 static void Main(string[] args)
72 {
73 TenXun tenXun = new TenXunGame("TenXun Game", "Have a new game published ....");
74 Subscriber lh = new Subscriber("Learning Hard");
75 Subscriber tom = new Subscriber("Tom");
76
77 // 添加订阅者
78 tenXun.AddObserver(new NotifyEventHandler(lh.ReceiveAndPrint));
79 tenXun.AddObserver(new NotifyEventHandler(tom.ReceiveAndPrint));
80
81 tenXun.Update();
82
83 Console.WriteLine("-----------------------------------");
84 Console.WriteLine("移除Tom订阅者");
85 tenXun.RemoveObserver(new NotifyEventHandler(tom.ReceiveAndPrint));
86 tenXun.Update();
87
88 Console.ReadLine();
89 }
90 }
91 }
```
从上面代码可以看出,使用事件和委托实现的观察者模式中,减少了订阅者接口类的定义,此时,.NET中的委托正式充到订阅者接口类的角色。使用委托和事件,确实简化了观察者模式的实现,减少了一个IObserver接口的定义,上面代码的运行结果如下图所示:
![](https://box.kancloud.cn/2016-01-23_56a2eb3da53dc.png)
## 四、观察者模式的适用场景
在下面的情况下可以考虑使用观察者模式:
* 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两者封装在独立的对象中以使它们可以各自独立地改变和复用的情况下。从方面的这个词中可以想到,观察者模式肯定在AOP(面向方面编程)中有所体现,更多内容参考:[Observern Pattern in AOP](http://www.cnblogs.com/idior/articles/229590.html).
* 当对一个对象的改变需要同时改变其他对象,而又不知道具体有多少对象有待改变的情况下。
* 当一个对象必须通知其他对象,而又不能假定其他对象是谁的情况下。
## 五、观察者模式的优缺点
观察者模式有以下几个优点:
* 观察者模式实现了表示层和数据逻辑层的分离,并定义了稳定的更新消息传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层,即观察者。
* 观察者模式在被观察者和观察者之间建立了一个抽象的耦合,被观察者并不知道任何一个具体的观察者,只是保存着抽象观察者的列表,每个具体观察者都符合一个抽象观察者的接口。
* 观察者模式支持广播通信。被观察者会向所有的注册过的观察者发出通知。
观察者也存在以下一些缺点:
* 如果一个被观察者有很多直接和间接的观察者时,将所有的观察者都通知到会花费很多时间。
* 虽然观察者模式可以随时使观察者知道所观察的对象发送了变化,但是观察者模式没有相应的机制使观察者知道所观察的对象是怎样发生变化的。
* 如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃,在使用观察者模式应特别注意这点。
## 六 总结
到这里,观察者模式的分享就介绍了。观察者模式定义了一种一对多的依赖关系,让多个观察者对象可以同时监听某一个主题对象,这个主题对象在发生状态变化时,会通知所有观察者对象,使它们能够自动更新自己,解决的是“当一个对象的改变需要同时改变多个其他对象”的问题。大家可以以微信订阅号的例子来理解观察者模式。
- C# 基础知识系列
- C# 基础知识系列 专题一:深入解析委托——C#中为什么要引入委托
- C# 基础知识系列 专题二:委托的本质论
- C# 基础知识系列 专题三:如何用委托包装多个方法——委托链
- C# 基础知识系列 专题四:事件揭秘
- C# 基础知识系列 专题五:当点击按钮时触发Click事件背后发生的事情
- C# 基础知识系列 专题六:泛型基础篇——为什么引入泛型
- C# 基础知识系列 专题七: 泛型深入理解(一)
- C# 基础知识系列 专题八: 深入理解泛型(二)
- C# 基础知识系列 专题九: 深入理解泛型可变性
- C#基础知识系列 专题十:全面解析可空类型
- C# 基础知识系列 专题十一:匿名方法解析
- C#基础知识系列 专题十二:迭代器
- C#基础知识 专题十三:全面解析对象集合初始化器、匿名类型和隐式类型
- C# 基础知识系列 专题十四:深入理解Lambda表达式
- C# 基础知识系列 专题十五:全面解析扩展方法
- C# 基础知识系列 专题十六:Linq介绍
- C#基础知识系列 专题十七:深入理解动态类型
- 你必须知道的异步编程 C# 5.0 新特性——Async和Await使异步编程更简单
- 全面解析C#中参数传递
- C#基础知识系列 全面解析C#中静态与非静态
- C# 基础知识系列 C#中易混淆的知识点
- C#进阶系列
- C#进阶系列 专题一:深入解析深拷贝和浅拷贝
- C#进阶系列 专题二:你知道Dictionary查找速度为什么快吗?
- C# 开发技巧系列
- C# 开发技巧系列 使用C#操作Word和Excel程序
- C# 开发技巧系列 使用C#操作幻灯片
- C# 开发技巧系列 如何动态设置屏幕分辨率
- C# 开发技巧系列 C#如何实现图片查看器
- C# 开发技巧 如何防止程序多次运行
- C# 开发技巧 实现属于自己的截图工具
- C# 开发技巧 如何使不符合要求的元素等于离它最近的一个元素
- C# 线程处理系列
- C# 线程处理系列 专题一:线程基础
- C# 线程处理系列 专题二:线程池中的工作者线程
- C# 线程处理系列 专题三:线程池中的I/O线程
- C# 线程处理系列 专题四:线程同步
- C# 线程处理系列 专题五:线程同步——事件构造
- C# 线程处理系列 专题六:线程同步——信号量和互斥体
- C# 多线程处理系列专题七——对多线程的补充
- C#网络编程系列
- C# 网络编程系列 专题一:网络协议简介
- C# 网络编程系列 专题二:HTTP协议详解
- C# 网络编程系列 专题三:自定义Web服务器
- C# 网络编程系列 专题四:自定义Web浏览器
- C# 网络编程系列 专题五:TCP编程
- C# 网络编程系列 专题六:UDP编程
- C# 网络编程系列 专题七:UDP编程补充——UDP广播程序的实现
- C# 网络编程系列 专题八:P2P编程
- C# 网络编程系列 专题九:实现类似QQ的即时通信程序
- C# 网络编程系列 专题十:实现简单的邮件收发器
- C# 网络编程系列 专题十一:实现一个基于FTP协议的程序——文件上传下载器
- C# 网络编程系列 专题十二:实现一个简单的FTP服务器
- C# 互操作性入门系列
- C# 互操作性入门系列(一):C#中互操作性介绍
- C# 互操作性入门系列(二):使用平台调用调用Win32 函数
- C# 互操作性入门系列(三):平台调用中的数据封送处理
- C# 互操作性入门系列(四):在C# 中调用COM组件
- CLR
- 谈谈: String 和StringBuilder区别和选择
- 谈谈:程序集加载和反射
- 利用反射获得委托和事件以及创建委托实例和添加事件处理程序
- 谈谈:.Net中的序列化和反序列化
- C#设计模式
- UML类图符号 各种关系说明以及举例
- C#设计模式(1)——单例模式
- C#设计模式(2)——简单工厂模式
- C#设计模式(3)——工厂方法模式
- C#设计模式(4)——抽象工厂模式
- C#设计模式(5)——建造者模式(Builder Pattern)
- C#设计模式(6)——原型模式(Prototype Pattern)
- C#设计模式(7)——适配器模式(Adapter Pattern)
- C#设计模式(8)——桥接模式(Bridge Pattern)
- C#设计模式(9)——装饰者模式(Decorator Pattern)
- C#设计模式(10)——组合模式(Composite Pattern)
- C#设计模式(11)——外观模式(Facade Pattern)
- C#设计模式(12)——享元模式(Flyweight Pattern)
- C#设计模式(13)——代理模式(Proxy Pattern)
- C#设计模式(14)——模板方法模式(Template Method)
- C#设计模式(15)——命令模式(Command Pattern)
- C#设计模式(16)——迭代器模式(Iterator Pattern)
- C#设计模式(17)——观察者模式(Observer Pattern)
- C#设计模式(18)——中介者模式(Mediator Pattern)
- C#设计模式(19)——状态者模式(State Pattern)
- C#设计模式(20)——策略者模式(Stragety Pattern)
- C#设计模式(21)——责任链模式
- C#设计模式(22)——访问者模式(Vistor Pattern)
- C#设计模式(23)——备忘录模式(Memento Pattern)
- C#设计模式总结
- WPF快速入门系列
- WPF快速入门系列(1)——WPF布局概览
- WPF快速入门系列(2)——深入解析依赖属性
- WPF快速入门系列(3)——深入解析WPF事件机制
- WPF快速入门系列(4)——深入解析WPF绑定
- WPF快速入门系列(5)——深入解析WPF命令
- WPF快速入门系列(6)——WPF资源和样式
- WPF快速入门系列(7)——深入解析WPF模板
- WPF快速入门系列(8)——MVVM快速入门
- WPF快速入门系列(9)——WPF任务管理工具实现
- ASP.NET 开发
- ASP.NET 开发必备知识点(1):如何让Asp.net网站运行在自定义的Web服务器上
- ASP.NET 开发必备知识点(2):那些年追过的ASP.NET权限管理
- ASP.NET中实现回调
- 跟我一起学WCF
- 跟我一起学WCF(1)——MSMQ消息队列
- 跟我一起学WCF(2)——利用.NET Remoting技术开发分布式应用
- 跟我一起学WCF(3)——利用Web Services开发分布式应用
- 跟我一起学WCF(3)——利用Web Services开发分布式应用
- 跟我一起学WCF(4)——第一个WCF程序
- 跟我一起学WCF(5)——深入解析服务契约 上篇
- 跟我一起学WCF(6)——深入解析服务契约 下篇
- 跟我一起学WCF(7)——WCF数据契约与序列化详解
- 跟我一起学WCF(8)——WCF中Session、实例管理详解
- 跟我一起学WCF(9)——WCF回调操作的实现
- 跟我一起学WCF(10)——WCF中事务处理
- 跟我一起学WCF(11)——WCF中队列服务详解
- 跟我一起学WCF(12)——WCF中Rest服务入门
- 跟我一起学WCF(13)——WCF系列总结
- .NET领域驱动设计实战系列
- .NET领域驱动设计实战系列 专题一:前期准备之EF CodeFirst
- .NET领域驱动设计实战系列 专题二:结合领域驱动设计的面向服务架构来搭建网上书店
- .NET领域驱动设计实战系列 专题三:前期准备之规约模式(Specification Pattern)
- .NET领域驱动设计实战系列 专题四:前期准备之工作单元模式(Unit Of Work)
- .NET领域驱动设计实战系列 专题五:网上书店规约模式、工作单元模式的引入以及购物车的实现
- .NET领域驱动设计实战系列 专题六:DDD实践案例:网上书店订单功能的实现
- .NET领域驱动设计实战系列 专题七:DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能
- .NET领域驱动设计实战系列 专题八:DDD案例:网上书店分布式消息队列和分布式缓存的实现
- .NET领域驱动设计实战系列 专题九:DDD案例:网上书店AOP和站点地图的实现
- .NET领域驱动设计实战系列 专题十:DDD扩展内容:全面剖析CQRS模式实现
- .NET领域驱动设计实战系列 专题十一:.NET 领域驱动设计实战系列总结