快轉到主要內容

EventBus 的神(精病)用法

·1330 字·3 分鐘
Denny Cheng / 月月冬瓜
作者
Denny Cheng / 月月冬瓜
獸控兼工程師兼鍵盤武術家

前言
#

前一篇提到 EventBus 使用字串 + 不定參數的作法,雖然我極度不欣賞,不過這也是動態語言的常規操作而已。如果去問不同人,可能會覺得這樣的手段也無傷大雅。
不過還有一種操作 EventBus 的手法,可能就連動態語言大師看了都會嘖嘖稱奇。

情境簡述
#

假想現在我要做一個 Blog,能提供發文和統計閱讀次數的功能。
當發一篇文章或編輯文章時,我並不在意閱讀次數。但當我瀏覽此文章時,我除了取得這篇文章的內容以外,我還需要顯示閱讀次數。
因此在取回文章時可能會收到如下資料

{
    "post_id": 3310,
    "content": "Hell Word",
    "read_count": 5
}

設計者想要去掉文章和統計兩者的耦合。因此有了兩個 module,其中一個處理文章,另一個處理統計。

文章 module 的 entity 可能會長這樣

class Post:
    post_id: int
    content: str

統計 module 的 entity 可能會長這樣

class Analytics:
    post_id: int
    read_count: int

這兩個 module 有著各自儲存方式,也有各自的修改方法。

由於文章是主體,簡單的作法可以在文章 module 中新增一個 get_post 的方法,然後讓文章 module 依賴統計 module。讓 get_post 不只取回文章,也取回統計。但這樣的作法就會讓文章和統計出現 dependency。

於是敝司先賢(?)就想出一種曠古絕今的神作法,既可以讓這兩個 module 不發生 dependency,又可以一次回傳所有資料。而且沒錯,就是用今天的主題—EventBus。

EventBus
#

方法也很簡單,在 post module 中先取得資訊,再發送到 event bus 上。

"""post module"""

def get_post(id: int):
    p = post_repo.get(id) # 假設這邊回傳的是一個 dict
    event_bus.send('expose_post', p) # 把取回的資料發送到 eventbus 上當參數
    return p # 回傳結果

在 analytics module 則是監聽特定 event。當收到 expose_post 的 event 時,就地修改此 event 的 payload。
由於這是 in-memory 操作,因此會反映在原始資料上。

""" analytics module"""

def expose_read_count_to_post(p):
    read_count = analytics_repo.get_read_count(p["id"]) # 取得 read_count
    p["read_count"] = read_count # 直接改寫 event bus 通知的物件。

event_bus.connect('expose_post', expose_read_count_to_post) # 註冊 event_bus

如此一來,就能讓 post 和 analytics module 在不 import 彼此的情況下,成功提供最終資料。

為什麼這是爛作法
#

雖然這作法光看就讓人吐血,應該不用再贅述爛在哪裡。但我還是試著條列一下:

  1. EventBus 是通知系統,是通知事件發生。事件本身應為 immutable。
  2. 承上,原本 Event 應該是收到通知的人互不干擾,但如果改了事件本身,那就極大可能就會需要考慮順序問題。(實際上在敝司的程式中,因為為發現某些事情要最後做,而在 EventBus 的 connect 裡面加了一個 final parameter 來表示這個 handler 要最晚做。)
  3. 雖然 Analytics 沒有 import Post module,但他其實完全知道 Post Module 在做什麼(不然就不會知道要在 post 加上 read_count 這個欄位)。這跟最初的「非耦合」理想相差甚遠。
  4. 由於 get_post 的資訊會提供給前端。除非你不寫文件,否則 Post module 也完全知道 Analytics module 在原來的 post 裡面加了什麼料。

會寫出這種設計,也有部份是 python 極其低能的 circular import 鍋,才會導致使用者想越過限制做了更奇怪的事情,不過這不在這次的討論範圍內。

好的作法
#

回過頭來看看

當發一篇文章或編輯文章時,我並不在意閱讀次數。但當我瀏覽此文章時,我除了取得這篇文章的內容以外,我還需要顯示閱讀次數。

這個問題其實是一個千古難題,目前比較好的作法是 CQRS (命令查詢職責分離) pattern。
經常我們會遇到讀取和寫入模型不匹配,而通常只有寫入模型需要遵守特定規則 (如發文章最少字元限制,或是回文資格限制等),讀取則沒那麼多限制。因此獨立 query module,並把 query module 當作更為全知的存在 (甚至可以直接操作 db ),是一種解決方法。
但太簡單的模型引入 CQRS 也會增加心智負擔,這種情況還不如就直接讓雙方產生耦合。該用什麼解法還是只能老話一句「看狀況」。

不過用 EventBus 絕對是錯誤的解法!!!