书接上文,委托是.Net Framework提供的类型安全的回调机制。委托本质上是类,类里面实现了带有方法指针的构造函数、Invoke、BeginInvoke和EndInvoke四个方法。当然这些工作都是CLR和编译器帮助我们完成的。事件建立在委托的基础上。CLR偷偷地帮我们把一个事件转换为一个私有字段和两个公有方法。一个私有字段是私有委托字段,两个公共方法是对委托字段的增加和移除的线程安全调用。本文重点不在于讨论委托和事件机制,而是通过事件如何方便的实现项目要求的内容。
为了方便论述,我再次描述项目要求。
有一个项目,有压力传感器、位移传感器、震动传感器等多种传感器每种类型有若干个传感器,对每个传感器做特定操作会引发历史曲线图、实时柱状图、实时数据表、状态显示等多种展示功能。
那么我们同样以测试驱动的方式先实现压力传感器通过特定操作实现实时数据表的功能。
测试代码段跟Observer拉模式是一样的,参见代码清单1,功能实现为代码清单2、3、4。
代码清单1 PressureSensorAndDataTableTest.cs
namespace RTChangeData
{
[TestFixture]
class PressureSensorAndDataTableTest
{
[Test]
public void PressureSensorChanged()
{
PressureSensorSubject ps = new PressureSensorSubject(5);
DataTableObserver dt = new DataTableObserver(ps);
ps.Channel1Data = 1;
ps.Channel2Data = 2;
Assert.AreEqual(ps.ID, dt.ID, "ID Not Equal.");
Assert.AreEqual(ps.Channel1Data, dt.Channel1Data, "Channel1 Data Not Equal.");
Assert.AreEqual(ps.Channel2Data, dt.Channel2Data, "Channel2 Data Not Equal.");
}
}
}
代码清单2 Subject.cs
namespace RTChangeDataByEvent
{
public interface Subject
{
event EventHandler<PSEventArgs> Channel1DataChanged;
event EventHandler<PSEventArgs> Channel2DataChanged;
}
public class PSEventArgs : EventArgs
{
public int NewValue { get; set; }
public int ID { get; set; }
public PSEventArgs(int id, int newValue)
{
ID = id;
NewValue = newValue;
}
}
}
代码清单3 PressureSensorSubject.cs
namespace RTChangeDataByEvent
{
public class PressureSensorSubject : Subject
{
private int m_id;
private int m_channel1Data;
private int m_channel2Data;
public int Channel1Data
{
get
{
return m_channel1Data;
}
set
{
m_channel1Data = value;
OnChannel1DataChanged(new PSEventArgs(m_id, value));
}
}
public int Channel2Data
{
get
{
return m_channel2Data;
}
set
{
m_channel2Data = value;
OnChannel2DataChanged(new PSEventArgs(m_id, value));
}
}
public object ID { get; set; }
public event EventHandler<PSEventArgs> Channel1DataChanged;
public event EventHandler<PSEventArgs> Channel2DataChanged;
public PressureSensorSubject(int id)
{
this.m_id = id;
}
protected virtual void OnChannel1DataChanged(PSEventArgs e)
{
//以线程安全的方式调用
EventHandler<PSEventArgs> tmp =
Interlocked.CompareExchange<EventHandler<PSEventArgs>>(ref Channel1DataChanged, null, null);
if (tmp != null)
{
tmp.Invoke(this, e);
}
}
protected virtual void OnChannel2DataChanged(PSEventArgs e)
{
//以线程安全的方式调用
EventHandler<PSEventArgs> tmp =
Interlocked.CompareExchange<EventHandler<PSEventArgs>>(ref Channel2DataChanged, null, null);
if (tmp != null)
{
tmp.Invoke(this, e);
}
}
}
}
代码清单4 DataTableObserver.cs
namespace RTChangeDataByEvent
{
class DataTableObserver
{
public object ID { get; set; }
public object Channel1Data { get; set; }
public object Channel2Data { get; set; }
public DataTableObserver(Subject ps)
{
ps.Channel1DataChanged += new EventHandler<PSEventArgs>((sender, e) =>
{
this.ID = e.ID;
Channel1Data = e.NewValue;
});
ps.Channel2DataChanged += new EventHandler<PSEventArgs>((sender, e) =>
{
Channel2Data = e.NewValue;
PressureSensorSubject pss = sender as PressureSensorSubject;
if (pss != null)
{
ID = pss.ID;
}
});
}
}
}
是不是发现类结构跟Observer很不一样?新技术往往能够放过来影响设计方式。下面先描述类图再来讲述采用事件实现的特点。
图1 事件观察者模式
从图1中可以看出,增加了参数类PSEventArgs,去掉了Observer类,而且倒置了Observer与Subject之间的依赖关系。同时DataTableObserver依赖于接口满足DIP原则。需要特殊说明的是在DataTableObserver中应该存在两个函数,这两个函数实现了触发Subject相应事件时应该执行的操作。在代码4中使用的lambda表达式。当短短两三行就能实现函数时使用lambda表达式是优雅直观的,如果需要很长的段落建议还是采用在构造函数中传入方法的新建委托对象方式。在图1的基础上实现项目的要求也很简单。可以针对不同的传感器设计不同的subject接口方式,在对应类中继承并实现接口。然后Observer需要观察哪个事件便注册相应事件即可。
结论:
设计模式是一种思想模式,其具体实现会根据语言的不同而发生微妙的变化。.Net中的委托和事件本来就是对回调的安全封装,是默认的观察者推模式的实现方案。因为.Net事件模式多数发生在界面上,当单线程访问时可以不使用Interlocked.CompareExchange方法,而是直接判断事件是否为空再调用事件。
关于事件与委托的异同,要涉及到CLR的很多细节部分,委托与反射配合使用也能给我们很多意想不到的惊喜,有时间我们另行讨论。
转载请注明出处,本文章的代码下载页:观察者模式事件实现方案