事件驅動模型 事件驅動模型是軟件系統平台中的一個重要區域
現代軟件系統大量地使用事件驅動的處理方法
尤其在用戶界面方面
雖然如此
過去在軟件開發語言中一直沒有融入事件處理的因子
直的出現
才將事件處理的工作負荷一部分的分派給編譯器
從而稍微減輕開發者的負擔
下圖顯示事件模型的組成份子

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裡有java
awt
event
XXXAdapter抽象輔助類)
另一個缺點則是必需要為每一類事件定義一個接口類型
即使可能大部分的事件只有極少的方法
微軟在為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
publisher
KeyPressedSink = &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
publisher
KeyPressed = 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) {
listenerList
add(l);
}
public void fireKeyPressedEvent(int keyCode) {
Iterator iter = erator();
while (iter
hasNext()) {
KeyListener l = (KeyListener)iter
next();
l
keyPressed(keyCode);
}
}
}
當然這段代碼只是簡單的示意
如果要考慮多線程的安全問題
可能要在addKeyListener()前面加上synchronized
還有
有預訂就必然相應的有
退訂
(removeXXXListener())
在這裡就不再把它寫出來
如果每個publisher都要這樣重復撰寫這樣的代碼的確很麻煩
所以中勢必希望能夠提供一種用來幫助publisher記錄預訂者
和發布事件的工具
按一般設計者的初步想法
一定是先提供一個輔助類來協助:

從語法上考慮簡化
add/remove動作應該可以用C++的operator=()
operator+=()和operator
=()來完成
像這樣:
Publisher publisher = new Publisher();
publisher
KeyEventHandlerDelegate += KeyPressedHandler;
//等同於
//publisher
EventHandlerDelegate
add(KeyPressedHandler);
如果可以這樣撰寫的話
確實很簡單
不過在強制性類型(strongly
typed)語言系統中
必需精確的定義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();
publisher
KeyPressed = OnKeyPressed; //預訂
publisher
KeyPressed += OnKeyPressed
; //預訂另一個
這樣一個delegate不僅可以幫忙記錄一個以上的subscribers
也可以簡單的通過一行的調用語句來發布事件
其實編譯器會為每一個delegate 生成一個相應的類來幫助處理這些工作
不過是作為一個只是編寫應用系統的程序員是不必要去了解這些細節的
有興趣的人可以去研究System
Delegate和System
MulticastDelegate 類
上面看到的結果應該已經是比較滿意的
但是仍有改善空間
首先
因為一個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();
// !!! 不允許 !!! 會編譯錯誤 !!!
publisher
KeyPressed(
);
接著
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中
推薦所有的事件數據類都繼承java
util
EventObject 類
因為在生成一個EventObject 對象的時候
必需給一個event source 對象作為參數
然後可以通過EventObject的getSource()方法來取得這個對象
在EventObject裡面
並沒有包含其他任何事件數據
所以如果在事件的傳遞過程當中
有任何事件數據需要傳遞
就必需從EventObject 派生出一個新的子類出來
如下圖

當中也有一個相似的類叫System
EventArgs
但是這個類的內容是空的
如下
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則仍保有其一貫簡約的風格
From:http://tw.wingwit.com/Article/program/net/201311/11899.html