(原创)无废话C#设计模式之十九:Observer
无废话C#设计模式之十九:Observer
意图
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。
场景
这次不说游戏了,假设我们需要在一个Web页面上分页显示数据。首先需要一个分页控制器和一个显示数据的表格。开始,客户的需求很简单,需要两个向前翻页向后翻页的按钮作为控制器,还需要一个GridView来显示数据。你可能会这么做:
l 在页面上放两个按钮和一个GridView控件
l 点击了下一页按钮判断是否超出了页面索引,如果没有的话更新GridView中的数据,然后更新控件的当前页,如果翻页后是最后一页的话把下一页按钮设置为不可用。
l 点击了上一页按钮判断是否超出了页面索引,如果没有的话更新GridView中的数据,然后更新控件的当前页,如果翻页后是第一页的话把上一页按钮设置为不可用。
在这里,我们的翻页控件仅仅和GridView进行依赖,看似问题不大。没有想到,客户看了Demo后觉得这样的体验不好,希望在页面上呈现当前页和总共页。于是,我们又在页面上加了一个Label控件,在按钮的点击事件里面再去更新Label控件的值。客户挺满意的,随着软件中数据越来越多,总页数达到了几十页,客户觉得前后翻页太不合理的了,希望有一个显示页数列表的分页控制器,客户的这个请求彻底使我们晕了,代码被我们修改的非常混乱:
l 点击了列表分页控件的页数后更新自身状态、通知GridView加载数据、通知按钮分页控件更新自身状态、通知Label更新页数信息。
l 点击了按钮分页控件后更新自身状态、通知GridView加载数据、通知列表分页控件更新自身状态、通知Label更新页数信息。
如果今后页面上还需要针对分页功能有任何修改的话,真不知道怎么去改。由此引入观察者模式来解决这些问题。
示例代码
using System; using System.Collections.Generic; using System.Text; namespace ObserverExample { class Program { static void Main(string[] args) { ButtonPager buttonPager = new ButtonPager(); ListPager listPager = new ListPager(); Control gridview = new GridView(); Control label = new Label(); buttonPager.changePageHandler += new Pager.ChangePageHandler(buttonPager.ChangePage); buttonPager.changePageHandler += new Pager.ChangePageHandler(gridview.ChangePage); buttonPager.changePageHandler += new Pager.ChangePageHandler(label.ChangePage); buttonPager.changePageHandler += new Pager.ChangePageHandler(listPager.ChangePage); listPager.changePageHandler += new Pager.ChangePageHandler(buttonPager.ChangePage); listPager.changePageHandler += new Pager.ChangePageHandler(gridview.ChangePage); listPager.changePageHandler += new Pager.ChangePageHandler(label.ChangePage); listPager.changePageHandler += new Pager.ChangePageHandler(listPager.ChangePage); buttonPager.NextPage(); Console.WriteLine(); buttonPager.NextPage(); Console.WriteLine(); buttonPager.NextPage(); Console.WriteLine(); buttonPager.PreviousPage(); Console.WriteLine(); buttonPager.PreviousPage(); Console.WriteLine(); listPager.SelectPage(2); Console.WriteLine(); listPager.SelectPage(1); Console.WriteLine(); listPager.SelectPage(0); } } abstract class Pager { protected int pageIndex = 0; public int PageIndex { get { return pageIndex; } set { pageIndex = value; } } protected int pageCount = 3; public int PageCount { get { return pageCount; } } public event ChangePageHandler changePageHandler; public delegate void ChangePageHandler(Pager sender); protected void ChangePage() { if (changePageHandler != null) changePageHandler(this); } } class ButtonPager : Pager, Control { public void NextPage() { if (pageIndex < pageCount - 1) { Console.WriteLine("Click NextPage Button..."); pageIndex++; ChangePage(); } } public void PreviousPage() { if (pageIndex > 0) { Console.WriteLine("Click PreviousPage Button..."); pageIndex--; ChangePage(); } } public void ChangePage(Pager sender) { base.pageIndex = sender.PageIndex; if (pageIndex > 0 && pageIndex < pageCount - 1) Console.WriteLine("<<Previous Next>>"); else if (pageIndex == 0) Console.WriteLine("Next>>"); else Console.WriteLine("<<Previous"); } } class ListPager : Pager, Control { public void SelectPage(int pageIndex) { if (pageIndex >= 0 && pageIndex < pageCount) { Console.WriteLine(string.Format("Click <{0}> Link...", pageIndex + 1)); base.pageIndex = pageIndex; ChangePage(); } } public void ChangePage(Pager sender) { base.pageIndex = sender.PageIndex; for (int i = 1; i <= pageCount; i++) { if (pageIndex + 1 == i) Console.Write(string.Format(" <{0}> ", i)); else Console.Write(string.Format(" {0} ", i)); } Console.WriteLine(); } } interface Control { void ChangePage(Pager sender); } class GridView : Control { public void ChangePage(Pager sender) { Console.WriteLine(string.Format("GridView->Show data of page {0}", sender.PageIndex + 1)); } } class Label : Control { public void ChangePage(Pager sender) { Console.WriteLine(string.Format("Label.Text=[{0}/{1}]", sender.PageIndex + 1, sender.PageCount)); } } } |
代码执行结果如下图:
代码说明
l 在这里,我们使用C#语言事件机制来实现观察者模式,虽然和GOF的“标准”模式不同,但是还是可以看出观察者模式最基本的几个角色。要知道,GOF设计模式虽然是经典,但是毕竟是很久以前提出的,可以考虑使用C#的一些特性来改进。
l Pager类型是抽象主体角色(或者叫作被观察者、发布方、主动方、目标、主题),传统的抽象主体用于保存观察者。在这里的ChangePage方法用于在有变化后触发事件。另外,从ChangePageHandler代理中看到,我们把抽象主体作为了参数,这样,观察者就能根据主体的状态作一些调整。
l ButtonPage是一个具体主体角色。NextPage()方法中首先判断请求的页面是否超过了页面索引,如果没有超过的话,则更新页面索引并且调用了基类的ChangePage()方法来通知所有的观察者。PreviousPage()方法也是一样的道理。
l Control接口是一个抽象观察者角色(或者说观察者、订阅方、被动方),它定义了一个统一的接口,如果接受到了事件通知,则调用这个方法进行处理。
l GridView和Label则是具体观察者,可以看到它们不用考虑怎么被通知的事情,只需要考虑被通知后做什么。在这里,GridView重新绑定了数据,Label显示了页数信息。
l 这样其实已经组成了一个最基本的观察者模式的结构。获取你也注意到了,ButtonPager还实现了Control接口,说明它还是一个具体的观察者。这并没有什么不可以,它一方面可以在翻页后通知GridView、Label等对象,一方面又可以被别人通知。还记得客户需要实现一个ListPager的需求吗?在ListPager翻页后还需要通知ButtonPager来改变状态呢。
l 一样的道理,ListPager也是一个观察者。它需要观察ButtonPager的变动。
l 注意到在ListPager和ButtonPager的ChangePage()方法中都更新了页面的索引值,你或许不理解为什么Label和GridView不更新呢?其实,这并没有什么奇怪,ButtonPager翻页后通知ListPager更新状态,最需要更新的状态就是页面索引值,用户不是直接点击ListPager翻页的,当然需要更新。Label和GridView中并没有实现是因为我们并没有实现具体的一些细节,在实际应用中这些控件保存一些状态也不奇怪。
l 最后来看一看怎么牵线搭桥。我们在ButtonPager的改变页面状态事件中注册了四个代理,也就是说它改变状态后需要通知四个观察者。怎么是四个呢?还包括它自己,从逻辑上可能难以理解,其实这是可行的重用代码的方案。对ButtonPager来说,是点击哪个控件翻页的并不重要,作为主体它的责任就是通知观察者,作为观察者它的责任就是更新状态或说对事件作出响应。
l 此例完整了一个四个观察者、两个主体的观察者模式。你可能角色一个类型既是观察者又是主体不可理解,其实这在现实生活中非常多的,生物链中的大部分生物既是观察者又是主体,“螳螂捕蝉,黄雀在后”中的螳螂就是。
l 再谈谈耦合和扩展。要再增加一个下拉框分页的分页控件怎么办?无须修改原来的代码,再写一个DropDownPager(继承Pager,实现Control),并且为它的修改分页事件和所有观察者挂钩就可以了。要再增加一个ListBox控件针对不同页数显示不同数据怎么办?也无须修改原来的代码,再写一个ListBox控件(实现Control),实现翻页响应的方法,并且订阅所有Pager的翻页事件即可。
l 注意,本例仅仅用来演示观察者模式的结构,并没有遵循.NET事件模型的最佳实践。
何时采用
通过这个例子,我们就很容易理解观察者模式的适用点了:
l 一个对象的行为引发其它多个对象的行为。前者成为主体,后者称为观察者。
l 为了降低耦合,不希望主体直接调用观察者的方法,而是采用动态订阅主体事件的方式来进行自动的连锁响应行为。
l 为了增加灵活性,希望动态调整订阅主体事件的观察者,或者希望动态调整观察者订阅主体的事件。
实现要点
l 抽象主体角色公开了自身的事件,可以给任意观察者订阅。
l 抽象观察者角色定义了统一的处理行为,在C#中使用事件-代理模式的话,统一的处理行为并不这么重要,有的时候甚至还会限制灵活性。由于本例的特殊原因,并没有从这个接口中得益。
l 响应方法订阅代理事件的操作可以在观察者中定义也可以在外部定义,根据自己的需求决定,放在外部定义灵活性更高。
l 具体观察者往往只需要实现响应方法即可。
l 可以有多个主体角色、多个观察者角色交错,也可以一个类型是两个角色,主体也可以提供多个事件。从应用上来说观察者模式变化是非常多的。
注意事项
l 由于这种灵活性,在观察者订阅事件的时候需要考虑是否会出现破坏行为?是否会出现无限循环或死锁等问题?观察者响应的时候是否会影响其它观察者?
l 对于观察者数量很多的时候使用观察者模式并不适合,可能会造成性能问题。
l 在不能采用事件-代理方式完成观察者模式的情况下(比如跨网络应用等)可以考虑采用传统的观察者模式。