觀察者模式最佳實踐,構建本身的一套事件分發系統

前言

試想這樣一個問題,當某個事件發生時,好比在遊戲中A模塊修改了用戶的金幣數,而B模塊和C模塊提供的功能都依賴於用戶的金幣數,那麼,A模塊在修改金幣數的同時,就須要通知B模塊和C模塊。常規的方法就是A模塊持有B模塊和C模塊的對象,而後分別經過調用對象接口的方式告訴它們,「嘿,我修改了用戶的金幣數,改爲了10金幣」。
但這樣就帶來了許多問題:git

  • A模塊引用了B模塊和C模塊,耦合嚴重
  • A模塊修改金幣數的方法中調用了B,C模塊的方法,當這兩個模塊發生變化時(好比B模塊接收金幣數的接口名稱改變了,或是C模塊再也不須要知道金幣數改變了),A模塊也要修改
  • 當又出現一個D模塊也須要知道金幣數的變化時,一樣須要修改A模塊以適應這種需求

爲了解決上面的問題,咱們天然想到了觀察者模式。github

觀察者模式

這裏簡單說一下什麼是觀察者模式:定義對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,它的全部依賴者(稱之爲觀察者)都會接收到通知並自動更新。
觀察者模式的好處是,對象之間是鬆耦合的,當一個對象改變狀態時,它並不須要知道本身的觀察者是誰,只須要發佈通知便可。任什麼時候候均可以增長或刪除觀察者,不會影響到發佈通知的對象。
而事件分發系統就是觀察者模式的一個具體實現函數

事件分發系統

事件分發系統核心須要提供的功能主要包括如下幾個部分:測試

  • 當一個對象發生改變時,能夠認爲此時產生了一個事件,提供一個派發事件的接口,以通知全部的觀察者
  • 須要提供註冊監聽事件的接口,以讓觀察者能夠訂閱本身須要接收的事件
  • 還需提供反註冊監聽事件接口,以讓觀察者能夠取消本身的訂閱
  • 最好還能在訂閱的時候設置優先級,優先級越高的能夠越先被通知

使用事件分發系統解決問題

首先,來看看使用事件分發系統處理上面提到的問題,會是什麼樣的效果。
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實現代碼。
實現事件分發系統時,須要當心一些特殊狀況,好比有如下幾個坑,讀者能夠留意一下代碼中對這幾個坑的處理設計

  • 在事件派發的過程當中訂閱該事件,訂閱還有優先級,須要當心處理排序問題
  • 在事件派發的過程當中取消訂閱該事件,須要採用標記移除,不能直接移除
  • 在事件派發的過程當中又派發了該事件,如何肯定事件派發完成

爲了便於講解,下面的代碼省略了一些非關鍵性的代碼,用--- ...代替。code

註冊監聽事件接口

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方法對象

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又被調用一次排序

反註冊事件監聽接口

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遞歸

事件派發接口

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 + 1event_listener.emit_count = event_listener.emit_count - 1來記錄事件的派發狀態,當emit_count > 0則代表事件還在派發過程當中。當emit_count == 0則代表事件派發完成。
不能使用event_listener.is_emiting = trueevent_listener.is_emiting = false代替的緣由是若是在觸發的回調中又派發了事件,造成了遞歸,那麼二次派發事件結束時會直接將event_listener.is_emiting置爲flase,致使一次派發事件對應的派發狀態被標記錯誤

更多

事件分發系統的完整源碼能夠點擊這裏查看,測試用例能夠點擊這裏查看
更多Lua相關的設計與使用,好比面向對象(代碼中用到的class關鍵字),組件系統,分模塊加載等等,能夠查看GitHub倉庫LuaKit

相關文章
相關標籤/搜索