前言#
前一篇提到 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 彼此的情況下,成功提供最終資料。
為什麼這是爛作法#
雖然這作法光看就讓人吐血,應該不用再贅述爛在哪裡。但我還是試著條列一下:
- EventBus 是通知系統,是通知事件發生。事件本身應為 immutable。
- 承上,原本 Event 應該是收到通知的人互不干擾,但如果改了事件本身,那就極大可能就會需要考慮順序問題。(實際上在敝司的程式中,因為為發現某些事情要最後做,而在 EventBus 的
connect裡面加了一個finalparameter 來表示這個 handler 要最晚做。) - 雖然 Analytics 沒有 import Post module,但他其實完全知道 Post Module 在做什麼(不然就不會知道要在 post 加上
read_count這個欄位)。這跟最初的「非耦合」理想相差甚遠。 - 由於
get_post的資訊會提供給前端。除非你不寫文件,否則 Post module 也完全知道 Analytics module 在原來的 post 裡面加了什麼料。
會寫出這種設計,也有部份是 python 極其低能的 circular import 鍋,才會導致使用者想越過限制做了更奇怪的事情,不過這不在這次的討論範圍內。
好的作法#
回過頭來看看
當發一篇文章或編輯文章時,我並不在意閱讀次數。但當我瀏覽此文章時,我除了取得這篇文章的內容以外,我還需要顯示閱讀次數。
這個問題其實是一個千古難題,目前比較好的作法是 CQRS (命令查詢職責分離) pattern。
經常我們會遇到讀取和寫入模型不匹配,而通常只有寫入模型需要遵守特定規則 (如發文章最少字元限制,或是回文資格限制等),讀取則沒那麼多限制。因此獨立 query module,並把 query module 當作更為全知的存在 (甚至可以直接操作 db ),是一種解決方法。
但太簡單的模型引入 CQRS 也會增加心智負擔,這種情況還不如就直接讓雙方產生耦合。該用什麼解法還是只能老話一句「看狀況」。
不過用 EventBus 絕對是錯誤的解法!!!
