觀察者模式最佳實踐,構建自己的一套事件分發系統
阿新 • • 發佈:2020-04-03
# 前言
試想這樣一個問題,當某個事件發生時,比如在遊戲中A模組修改了使用者的金幣數,而B模組和C模組提供的功能都依賴於使用者的金幣數,那麼,A模組在修改金幣數的同時,就需要通知B模組和C模組。常規的方法就是A模組持有B模組和C模組的物件,然後分別通過呼叫物件介面的方式告訴它們,“嘿,我修改了使用者的金幣數,改成了10金幣”。
但這樣就帶來了許多問題:
* A模組引用了B模組和C模組,耦合嚴重
* A模組修改金幣數的方法中呼叫了B,C模組的方法,當這兩個模組發生變化時(比如B模組接收金幣數的介面名稱改變了,或是C模組不再需要知道金幣數改變了),A模組也要修改
* 當又出現一個D模組也需要知道金幣數的變化時,同樣需要修改A模組以適應這種需求
為了解決上面的問題,我們自然想到了觀察者模式。
# 觀察者模式
這裡簡單說一下什麼是觀察者模式:定義物件之間的一對多依賴,這樣一來,當一個物件改變狀態時,它的所有依賴者(稱之為觀察者)都會接收到通知並自動更新。
觀察者模式的好處是,物件之間是鬆耦合的,當一個物件改變狀態時,它並不需要知道自己的觀察者是誰,只需要釋出通知即可。任何時候都可以增加或刪除觀察者,不會影響到釋出通知的物件。
而事件分發系統就是觀察者模式的一個具體實現
# 事件分發系統
事件分發系統核心需要提供的功能主要包括以下幾個部分:
* 當一個物件發生改變時,可以認為此時產生了一個事件,提供一個派發事件的介面,以通知所有的觀察者
* 需要提供註冊監聽事件的介面,以讓觀察者可以訂閱自己需要接收的事件
* 還需提供反註冊監聽事件介面,以讓觀察者可以取消自己的訂閱
* 最好還能在訂閱的時候設定優先順序,優先順序越高的可以越先被通知
## 使用事件分發系統解決問題
首先,來看看使用事件分發系統處理上面提到的問題,會是什麼樣的效果。
A模組只需要派發金幣修改事件,B,C模組只需要訂閱金幣修改事件,之後便可以收到通知了。是不是很簡單呢
```lua
local B = class()
function B:on_money_change( money )
print(money, "B receive event")
end
-- 訂閱金幣修改事件
EventSystem:on(Event.MoneyChanged, B.on_money_change, {target = B})
local C = class()
function C:on_money_change( money )
print(money, "C receive event")
end
EventSystem:on(Event.MoneyChanged, C.on_money_change, {target = C})
-- 在A模組中派發金幣修改事件,當前金幣為10
EventSystem:emit(Event.MoneyChanged, 10)
```
接下來會仔細解讀一下這個`EventSystem`事件分發系統的Lua實現程式碼。
實現事件分發系統時,需要小心一些特殊情況,比如有以下幾個坑,讀者可以留意一下程式碼中對這幾個坑的處理
* 在事件派發的過程中訂閱該事件,訂閱還有優先順序,需要小心處理排序問題
* 在事件派發的過程中取消訂閱該事件,需要採用標記移除,不能直接移除
* 在事件派發的過程中又派發了該事件,如何確定事件派發完成
為了便於講解,下面的程式碼省略了一些非關鍵性的程式碼,用`--- ...`代替。
## 註冊監聽事件介面
```lua
function EventSystem:on( event, func, params )
--- ...
local event_listener = self._listeners[event]
params = params or {}
local priority = params.priority or 0
local target = params.target
--- ...
local cb = {target = target, func = func, id = id, priority = priority}
table.insert(event_listener.list, cb)
id = id + 1
if priority > 0 then
event_listener.need_sort = true
self:sort(event_listener)
end
end
```
`on`方法中`event`引數表示要註冊監聽的事件名稱,`func`引數表示當事件發生時要觸發的回撥函式,`params`表示額外引數,可以設定註冊監聽的目標`target`(可以利用它反註冊所有與其相關的監聽),也可以設定要註冊監聽的優先順序,優先順序越高的越先執行
`on`方法的實現還是比較簡單的,主要就是將註冊的相關資訊插入到`event_listener`表中,但是明明註冊的監聽是有優先順序的,卻仍然只是呼叫`table.insert`將資訊插入到表的末尾,這是為什麼呢?讀者可以先留意一下,後面會有詳細解釋。
還需要格外注意的是`sort`方法
```lua
function EventSystem:sort( listener )
if listener.need_sort == true and listener.emit_count == 0 then
table.sort(listener.list, function ( a, b )
if a.priority == b.priority then
return a.id < b.id
else
return a.priority > b.priority
end
end)
listener.need_sort = false;
end
end
```
可以看到`sort`方法必須在`listener.emit_count == 0`時才會進行排序,`listener.emit_count == 0`表示的是當前的事件沒有處於派發狀態,後面講到派發介面時會詳細解釋,這裡讀者只需要知道其表示的含義即可。
事件處於派發狀態時不能進行優先順序排序原因是可能會造成回撥的重複觸發。
比如當前事件有4個回撥 a, b, c, d,派發事件是順序執行回撥,當執行到第3個回撥c時
如果在c回撥中又註冊了一個優先順序最高的回撥e,立刻排序的話,e插入到第一位,c會被擠到第4位,順序執行到第4個回撥時,導致c又被呼叫一次
## 反註冊事件監聽介面
```lua
function EventSystem:off( event, func, params )
--- ...
local event_listener = self._listeners[event]
params = params or {}
for i,cb in ipairs(event_listener.list) do
if cb.func == func and cb.target == params.target then
if event_listener.emit_count > 0 then
-- 派發過程中只進行標記刪除
cb.need_remove = true
event_listener.need_clean = true
else
table.remove(event_listener.list, i)
end
break;
end
end
end
```
`off`方法用於取消事件監聽,當事件未處於派發過程中時,直接呼叫`table.remove`移除註冊資訊即可,但當事件處於派發過程中時,不能直接移除,只能先進行標記
在事件處於派發過程中時不能直接移除的原因是可能導致遺漏觸發某些回撥
比如當前事件有5個回撥 a, b, c, d, e,順序執行到第3個回撥c時
如果在c回撥中呼叫了`off`方法取消自己的監聽,此時直接移除c的話,會導致d回撥移動到第3位,e移動到第4位,順序執行到第4個回撥時,呼叫的是e而遺漏了d
## 事件派發介面
```lua
function EventSystem:emit( event, ... )
--- ...
local event_listener = self._listeners[event]
local interrupt = false
local length = #event_listener.list
-- 這裡不能使用ipairs,確保不會觸發在派發過程中註冊的事件
-- 只取當前已經註冊的事件數量,如果在派發過程中再註冊(呼叫了table.insert),本次派發也不會呼叫
for i = 1, length do
if interrupt == true then
break
end
local cb = event_listener.list[i]
if cb.func and cb.need_remove ~= true then
event_listener.emit_count = event_listener.emit_count + 1
if cb.target then
interrupt = cb.func(cb.target, ...)
else
interrupt = cb.func(...)
end
event_listener.emit_count = event_listener.emit_count - 1
end
end
self:sort(event_listener);
self:clean(event_listener);
return interrupt
end
```
`emit`方法負責派發一個事件,順序執行`event_listener`中註冊的回撥。事件的派發支援中斷,當執行某個回撥時,如果這個回撥返回了`true`則可以中斷當前事件的派發。
值得一提的是,程式碼通過對應的`event_listener.emit_count = event_listener.emit_count + 1`和`event_listener.emit_count = event_listener.emit_count - 1`來記錄事件的派發狀態,當`emit_count > 0`則表明事件還在派發過程中。當`emit_count == 0`則表明事件派發完成。
不能使用`event_listener.is_emiting = true`和`event_listener.is_emiting = false`代替的原因是如果在觸發的回撥中又派發了事件,形成了遞迴,那麼二次派發事件結束時會直接將`event_listener.is_emiting`置為`flase`,導致一次派發事件對應的派發狀態被標記錯誤
# 更多
事件分發系統的完整原始碼可以點選[這裡](https://github.com/iwiniwin/LuaKit/blob/master/core/event/event_system.lua)檢視,測試用例可以點選[這裡](https://github.com/iwiniwin/LuaKit/blob/master/test.lua)檢視
更多Lua相關的設計與使用,比如面向物件(程式碼中用到的class關鍵字),元件系統,分模組載入等等,可以檢視GitHub倉庫[LuaKit](https://github.com/iwiniwin