事件驱动模型事件驱动模型是软件系统平台中的一个重要区域现代软件系统大量地使用事件驱动的处理方法尤其在用户界面方面虽然如此过去在软件开发语言中一直没有融入事件处理的因子直的出现才将事件处理的工作负荷一部分的分派给编译器从而稍微减轻开发者的负担
下图显示事件模型的组成份子
Subscriber需事先和publisher预订要接受其发布的某事件(下图a)publisher在某事件发生以后必需先生成该事件的相关数据对象(下图a)然后通过方法调用来通知subscriber(下图a)也就是用回调(callback)的方式来通知subscriber当然在预订的时候并不一定要由subscriber自身来预订也可以由另一个对象来帮忙预订其动态图形示意如下
本文并不探讨异步的信息传送也就是在整个事件的处理过程当中publisher和subscriber 对象皆需要同时存在如果对于离线(offline)的方式来处理事件有兴趣的话请参阅Java的JMS(Java Message Service)和NET的LCE(Loosely Coupled Events)
事件是什么?
那么到底事件是什么?在软件系统中要如何表达一个事件?一个事件应该包括两个东西识别事件的名称(event identity)和事件的相关的数据(event data)例如一个键盘按键被按下的事件可能叫KeyPressedEvent事件数据则为该按键的代码
先前提到发布事件是用调用方法的方式(回调)不过有一个问题就是publisher无法事先知道subscriber的类型在Java的编码模式当中回调可以使用接口模式也就是publisher必需事先定义好一个在发布事件中使用的接口subscriber实现该接口中的方法publisher则通过调用接口中的方法来完成发布事件的工作如下图
这样在Java的编码模式中一个事件的识别名称就是接口名称和其中的方法名称而事件数据则自然是接口方法的参数了Java对于这个接口的命名风格为XXXListener顾名思义就是某事件的倾听者例如
public interface KeyListener extends EventListener {
public void keyTyped(KeyEvent e);
public void keyPressed(KeyEvent e);
public void keyReleased(KeyEvent e);
}
由于一个接口中可以包含多个方法所以Java在设计事件的时候是将一组相关联的事件放在一起这样设计的优点是可以很好的将事件做分类并且在publisher中如果要处理的事件较多的话可以使用比较少的成员变量来记录subscribers缺点是如果subscriber只对事件接口中的部分事件有兴趣也必需要全盘实现该接口(所以在AWT里有javaawteventXXXAdapter抽象辅助类)另一个缺点则是必需要为每一类事件定义一个接口类型即使可能大部分的事件只有极少的方法
微软在为C#语言命名的时候就刻意隐喻C#是从C/C++为基础发展而得的面向对象程序语言始祖绝不是Java所以肯定要保留一些C/C++的语言机制在C/C++里面对回调的设计方式就是用函数指针想当然C#也希望直接使用类似函数调用的方式来做为事件发布的方法如下图
所以C#期望使用函数指针类型来作为事件的识别名称然后用函数的参数来传递事件数据我们先用一段C++代码来描绘这幅图画
Event type definition:
// 定义KeyPressedCallback 为一个函数指针的类型
// 该函数接受一个整数型参数无返回值
typedef void (*KeyPressedCallback)(int keyCode);
Publisher:
class Publisher
{
public KeyPressedCallback KeyPressedSink = null;
void FireEvent(int KeyCode)
{
if (KeyPressedSink != null)
(*KeyPressedSink)(keyCode);//callback
}
}
Subscriber:
void KeyPressedHandler(int keyCode)
{
}
Publisher publisher = new Publisher();
//reGISter
publisherKeyPressedSink = &KeyPressedHandler;
一个当代的纯面向对象程序语言是肯定希望要把造成程序复杂和不易维护的指针给去除的所以在C#语言机制当中势必要创造新的元素来取代于是delegate(委托)出现了如下
Event type definition:
// 定义KeyPressedDelegate 为一个类似函数指针的类型
// 该函数接受一个整数型参数无返回值
delegate void KeyPressedDelegate(int keyCode);
Publisher:
class Publisher
{
public KeyPressedDelegate KeyPressed = null;
void FireEvent(int KeyCode)
{
if (KeyPressed != null)
KeyPressed(keyCode);
}
}
Subscriber:
void KeyPressedHandler(int keyCode)
{
}
Publisher publisher = new Publisher();
//register
publisherKeyPressed = KeyPressedHandler;
一开始你可以把KeyPressedDelegate当成是与函数指针相类似的东西通过它你可以引用一个实例方法或静态方法就好像引用一个对象一样然后可以通过这个delegate直接调用其引用的方法但是下面你会看到delegate更扩大了其引用能力
事件的预订和发布
Publisher必需能够接受多个subscribers的预订所以在publisher当中必需维护预订者的列表以供将来发布事件使用在Java语言的模式中并没有提供特别的东西来帮助这件事可以自己用collection 类来做例如可以使用ArrayList 对象来做记录Java的预订方法名的风格为addXXXListener()因为在publisher端必需把subscriber 对象添加到预订者列表后面如下图
对于subscriber来说预订动作的内部处理是黑箱的subscriber不用关心publisher是如何做预订记录的参考以下代码片段
class Publisher {
private ArrayList listenerList = new ArrayList();
public void addKeyListener(KeyListener l) {
listenerListadd(l);
}
public void fireKeyPressedEvent(int keyCode) {
Iterator iter = erator();
while (iterhasNext()) {
KeyListener l = (KeyListener)iternext();
lkeyPressed(keyCode);
}
}
}
当然这段代码只是简单的示意如果要考虑多线程的安全问题可能要在addKeyListener()前面加上synchronized还有有预订就必然相应的有退订(removeXXXListener())在这里就不再把它写出来
如果每个publisher都要这样重复撰写这样的代码的确很麻烦所以中势必希望能够提供一种用来帮助publisher记录预订者和发布事件的工具按一般设计者的初步想法一定是先提供一个辅助类来协助:
从语法上考虑简化add/remove动作应该可以用C++的operator=()operator+=()和operator=()来完成像这样:
Publisher publisher = new Publisher();
publisherKeyEventHandlerDelegate += KeyPressedHandler;
//等同于
//publisherEventHandlerDelegateadd(KeyPressedHandler);
如果可以这样撰写的话确实很简单不过在强制性类型(stronglytyped)语言系统中必需精确的定义add()方法参数中的delegate 类型这样似乎无法写出一个可以公用的基础类所以在NET中是结合delegate关键字通过简单的语法借助编译器来帮我们自动生成相关的代码于是把delegate的能力再予以加强了
Event type definition:
public delegate void KeyPressedDelegate(int keyCode);
Publisher:
class Publisher
{
public KeyPressedDelegate KeyPressed;
void FireKeyPressedEvent(int KeyCode)
{
if (KeyPressed != null)
//依次调用记录在KeyPressed中的所有方法
KeyPressed(keyCode);
}
}
Subscriber:
void OnKeyPressed(int keyCode)
{
}
void OnKeyPressed(int keyCode)
{
}
Publisher publisher = new Publisher();
publisherKeyPressed = OnKeyPressed;//预订
publisherKeyPressed += OnKeyPressed;//预订另一个
这样一个delegate不仅可以帮忙记录一个以上的subscribers也可以简单的通过一行的调用语句来发布事件其实编译器会为每一个delegate 生成一个相应的类来帮助处理这些工作不过是作为一个只是编写应用系统的程序员是不必要去了解这些细节的有兴趣的人可以去研究SystemDelegate和SystemMulticastDelegate 类
上面看到的结果应该已经是比较满意的但是仍有改善空间首先因为一个delegate成员是public的任何人都可以任意的直接接触有失面向对象世界中的信息封装和隐藏(information encapsulation and hiding)的原则所以在C#中又增加一个关键字event用在放在声明一个delegate成员变量的前面这样表示只有在声明这个delegate的类内部才可以直接对它进行subscriber 调用
public delegate void KeyPressedDelegate(int keyCode);
class Publisher
{
public event KeyPressedDelegate KeyPressed;
void FireKeyPressedEvent(int KeyCode)
{
if (KeyPressed != null)
//只有在Publisher才可以
KeyPressed(keyCode);
}
}
// outside of Publisher
Publisher publisher = new Publisher();
// !!! 不允许 !!! 会编译错误 !!!
publisherKeyPressed();
接着event delegate是以一个成员变量的方式存在如果能以属性的方式让外界进行存取不是更好吗于是又增加了event Accessors在C#语言中是使用add和remove来封装实际的 += 和 = 操作如下
class Publisher
{
protected event KeyPressedDelegate m_KeyPressed;
// event accessor定义一个事件属性
public event KeyPressedDelegate KeyPressed
{
add
{
m_KeyPressed += value;
}
remove
{
m_KeyPressed = value;
}
}
void FireKeyPressedEvent(int KeyCode)
{
if (KeyPressed != null)
m_KeyPressed(keyCode);
}
}
不管是事件变量或者是事件属性对声明事件变量和属性的类的外部只能对它做 += 和 = 操作这样可以加强它的安全性当然event accessor只有add和remove操作所以不管是任何人(包括声明该事件属性的类内部)也只能对事件属性做 += 和 = 操作
经过这样的改善可以理论上更减弱publisher和subscriber之间的耦合力了
事件数据
接下来我们谈一谈另一个在事件模型中的重要角色就是在事件发布中被传递的事件数据
一个subscriber在接受同一种事件的时候可能来自不同的publisher所以自然地希望知道发出事件的人是谁也就是在传递的参数当中必需包含一个publisher 对象的引用在Java中推荐所有的事件数据类都继承javautilEventObject 类因为在生成一个EventObject 对象的时候必需给一个event source 对象作为参数然后可以通过EventObject的getSource()方法来取得这个对象在EventObject里面并没有包含其他任何事件数据所以如果在事件的传递过程当中有任何事件数据需要传递就必需从EventObject 派生出一个新的子类出来如下图
当中也有一个相似的类叫SystemEventArgs但是这个类的内容是空的如下
public class EventArgs
{
public static readonly EventArgs Empty;
static EventArgs()
{
Empty = new EventArgs();
}
public EventArgs()
{}
}
NET认为不一定所有的subscriber都对event source感兴趣所以如果需要的话就把event source当成是delegate方法的参数来传递好了NET定义了一个标准的delegate EventHandler以下是它的签名(signature):
public delegate void EventHandler(object sender EventArgs e);
以后只要你需要的delegate的签名与EventHandler相同的话就直接用它了这里所谓的签名相同是指参数的类型和返回值的类型皆相同
Java和NET都希望用户在定义的事件数据类的时候尽可能的使用推荐的基类因为这样在publisher对发出的事件数据内容有所变更或扩大的时候对subscriber的沖击会比较小这是由于多型(polymorphism)机制的帮助
结束语
经过这番解析之后应该能够比较清楚的了解到Java和NET事件处理框架的设计思路希望有助于读者更进一步理解其框架的形成过程从语言的角度来看NET的确有一些针对性的改善和试图简化对事件的处理Java则仍保有其一贯简约的风格