Observer模式即观察者模式,该模式的作用是观察者(Observer)能实时知晓被观察主体(Subject)的变化。实现实时知晓主体变化可有多种方式,常用的有主动查询和被动通知两种方式。主动查询可采用Observer实时轮询Subject状态的方式,但缺点是抢占CPU资源,作为单一功能的嵌入式软件可以使用,但是用在多任务系统中,用户是无法忍受的;被动通知可通过协商解决,当subject特定状态改变时主动告诉observer我的哪些状态变化了。那么如何协商呢?在静态语言中协商往往通过定义接口、抽象类实现。
观察者模式便是上述问题的第二种解决方案。该解决方案存在两种实现方式,分别叫做拉模式和推模式。我们通过下面项目的实现来具体阐述。
有一个项目,有压力传感器、位移传感器、震动传感器等多种传感器每种类型有若干个传感器,对每个传感器做特定操作会引发历史曲线图、实时柱状图、实时数据表、状态显示等多种展示功能。
我们如何实现呢,人类还是善于通过一小步一小步的努力最终实现了大的进步,就像《士兵突击》中七连长对许三多的评价,一不小心别人手里的小草就变成了参天大树。而历史也证明了大跃进的危害。
所以我们先考虑项目中只有一个压力传感器只显示实时数据表的功能。那么自然分出三个关键词:压力传感器、数据表、实时。功能如图1所示:
图1 数据传输图例
对应的类图如图2所示:
图2 压力传感器、数据表类图
图2很简单,但是很明显违反了DIP(依赖倒置原则DependenceInversion Principle),特别是我们可以预见PressureSensorSubject不能依赖于DataTableObserver这个具体类。因此该类图被设计为图3的样子。
图3符合DIP的压力传感器、数据表类图
下面我们采用测试驱动的方式逐步实现项目要求的功能,也许在编码中会发现设计不合理的地方。我们需要构建的测试内容是当PressureSensor数据改变时,DataTable中的数据是否也同时发生了改变。测试代码如代码1所示:
代码清单 1 PressureSensorAndDataTableTest.cs[TestFixture]
class PressureSensorAndDataTableTest
{
[Test]
public void PressureSensorChanged()
{
PressureSensorSubject ps1 = new PressureSensorSubject();
DataTableObserver dt = new DataTableObserver();
ps1.AddObserver(dt);
ps1.ID = 1;
ps1.Channel1Data = 1;
ps1.Channel2Data = 2;
Assert.AreEqual(ps1.ID, dt.ID, "ID Not Equal.");
Assert.AreEqual(ps1.Channel1Data, dt.Channel1Data, "Channel1 Data Not Equal.");
Assert.AreEqual(ps1.Channel1Data, dt.Channel2Data, "Channel1 Data Not Equal.");
}
}
实现过程中我们添加了ID的属性,超出了当初的设计。目前测试代码是不能正确执行的,所以我们下一步要实现类 PressureSensorSubject 和 DataTableObserver以让代码正确运行。实现过程见代码清单2、3、4
代码清单2 PressureSensorSubject.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplicationTest
{
class PressureSensorSubject
{
private Observer m_observer = null;
private int m_channel1Data;
private int m_channel2Data;
public int Channel1Data
{
get
{
return m_channel1Data;
}
set
{
m_channel1Data = value;
Notice();
}
}
public int ID { get; set; }
public int Channel2Data
{
get
{
return m_channel2Data;
}
set
{
m_channel2Data = value;
Notice();
}
}
public PressureSensorSubject(int id)
{
ID = id;
}
public void AddObserver(Observer obs)
{
m_observer = obs;
}
protected void Notice()
{
if (m_observer != null)
{
m_observer.update();
}
}
}
}
代码清单 3 Observer.cs
namespace ConsoleApplicationTest
{
interface Observer
{
void update();
}
}
代码清单 4 DataTableObserver.cs
namespace ConsoleApplicationTest
{
class DataTableObserver : Observer
{
public int Channel1Data { get; set; }
public int Channel2Data { get; set; }
public int ID { get; set; }
private PressureSensorSubject m_psSubject;
public DataTableObserver(PressureSensorSubject ps)
{
m_psSubject = ps;
}
public void update()
{
this.ID = m_psSubject.ID;
this.Channel1Data = m_psSubject.Channel1Data;
this.Channel2Data = m_psSubject.Channel2Data;
}
}
}
代码的实现已经改变了我们当初的设计,很神奇吧。常常会出现在实现的过程中因为需求或者思路的变化改变当初设计的情形。那么,我们现在的类图如图4所示。
图4 代码实现方案中PressureSensorSubject和DataTableObserver调用关系
再来分析这个图我们会发现图4也违反了DIP原则,DataTableObserver类只能依赖于具体类PressureSensorSubject,我们可以忍受这个缺陷吗?如果只有一种压力传感器而不存在位移传感器、震动传感器那么这个模型是暂时可以忍受的,但事与愿违,我们必须要打破这种严重的依赖结构,所以变成了图5的样子。
图5观察者模式——拉模式
这就是Observer的拉模式,之所以称之为拉模式在于DataTableObserver得到通知后需要访问Subject(及其子类)以获取自己需要得到的内容。
这种模式存在的优点是实现起来比较简单,缺点是如果subject子类有非常多的属性时,如何知道哪个属性发生了变化。如果subject能主动通知observer自己变化的属性且把自身信息也传递给observer不就解决问题了吗?是的,这便是观察者模式中的推模式。这种在Windows界面开发中经常使用(一般一个参数是sender表示发生变化的对象,应一个是parameter含有变化的各个参数)。
测试方案基本没有发生改变,唯一的变化是 DataTableObserver的对象不在引用 DataTableObserver的对象(清单5)。下面主要把推模式的完整代码贴出来(代码清单6、7、8、9)。代码清单5 单元测试 TestPressureSensorAndDataTable.cs
namespace RTChangeData
{
[TestFixture]
class TestPressureSensorAndDataTable
{
[Test]
public void PressureSensorChanged()
{
PressureSensorSubject ps1 = new PressureSensorSubject(5);
DataTableObserver dt = new DataTableObserver();//改变的部分
ps1.AddObserver(dt);
ps1.Channel1Data = 1;
ps1.Channel2Data = 2;
Assert.AreEqual(ps1.ID, dt.ID, "ID Not Equal.");
Assert.AreEqual(ps1.Channel1Data, dt.Channel1Data, "Channel1 Data Not Equal.");
Assert.AreEqual(ps1.Channel2Data, dt.Channel2Data, "Channel2 Data Not Equal.");
}
}
}
代码清单6 Subject.cs
namespace RTChangeData
{
abstract class Subject
{
protected Observer m_observer = null;
public void AddObserver(Observer obs)
{
m_observer = obs;
}
abstract protected void Notice(int newValue, string hint);
}
}
代码清单 7 PressureSensorSubject.cs
namespace RTChangeData
{
class PressureSensorSubject : Subject
{
private int m_channel1Data;
private int m_channel2Data;
public int Channel1Data
{
get
{
return m_channel1Data;
}
set
{
m_channel1Data = value;
Notice(value, "Channel1");
}
}
public int ID { get; set; }
public int Channel2Data
{
get
{
return m_channel2Data;
}
set
{
m_channel2Data = value;
Notice(value, "Channel2");
}
}
public PressureSensorSubject(int id)
{
ID = id;
}
protected override void Notice(int newValue, string hint)
{
if (m_observer != null)
{
m_observer.update(this.ID, newValue, hint);
}
}
}
}
代码清单8 Observer.cs
namespace RTChangeData
{
interface Observer
{
void update(int senderID, int newValue, string hint);
}
}
代码清单9 DataTableObserver.cs
namespace RTChangeData
{
class DataTableObserver : Observer
{
public int Channel1Data { get; set; }
public int Channel2Data { get; set; }
public int ID { get; set; }
public void update(int senderID, int newValue, string hint)
{
this.ID = senderID;
if (hint.Equals("Channel1"))
{
this.Channel1Data = newValue;
}
else
{
this.Channel2Data = newValue;
}
}
}
}
有的同学可能提出hint采用字符串的提示方式并不好,这可能引起调用时的误操作。的确是存在这个问题。在示例程序中这么处理,同时违背了接口单一原则(ISP)。将接口subject分为两个接口或许更合理一些。这个工作就交给读者来完成吧。图6便是推模式的类图。
图5观察者模式——推模式
结合项目要求,还需要解决两个问题:
1、如何在事件触发时通知多个Observer对象?
可以在Subject中维护ArrayList变量以存储Observer,当触发事件时在Notice中逐个通知Observer对象即可。
2、如何实现不同类型的传感器在特定操作下引发不同的显示?
多种不同的传感器可通过继承Subject,实现的不同子类。要实现包括实时柱状图、历史曲线功能、状态显示等功能可以设计多个Subject接口。Observer子类注册相应接口就可以得到对应事件通知。
结论:
本文按照项目要求,逐步导出了Observer模式的两种实现方式。在实际应用中,往往当需求改变时,才在原有系统的基础上渐变的增加设计模式。观察者模式可以将通知者和被通知者两个模块解除耦合分开开发。这与.Net中的delegate以及Event非常相似,delegate完成了注册和逐个通知的工作,将会减少我们的工作量。同时也将抽象类subject方便的替代成接口。采用Event的实现模式将会在后续的文章中介绍。
内容下载可到:我的下载页
版权所有,转载请注明出处。