iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Software Development

當 AI 寫得比你讀得快:Code Review 該審什麼系列 第 12 篇

Day 12:案例拆解——Event/Listener 機制解決了不存在的問題

  • 分享至 

  • xImage
  •  

前言:「事件驅動架構,不是為了將來的擴充性嗎?」

「先把事件機制建好,以後要加新功能只要多掛一個 Listener」——這句話你可能自己也講過,聽起來完全合理:今天先花一點成本把架構搭好,將來省下大改的成本。

今天要拆的案例會告訴你,這個邏輯有一個經常被忽略的前提:「將來會不會真的用到」跟「現在能不能想像它被用到」,是兩件不同的事。 版本 B 裡的 Event/Listener 機制,就是後者的產物。

今日目標

  • 理解 Event-Driven(事件驅動)機制原本要解決的問題:解耦「觸發事件的一方」跟「處理事件的一方」
  • 看懂版本 B 裡事件系統的真實使用規模,跟它「架構上支援的規模」之間的落差
  • 認識 YAGNI(You Aren't Gonna Need It)原則在這個案例裡具體對應到什麼
  • 理解「這個機制不影響任何業務結果」為什麼本身就是一個警訊,而不是優點
  • 學會分辨「保留擴充可能性」跟「真的需要」的差異

版本 B 的事件系統:65 種事件、62 種監聽器,只有一組真正被用到

版本 B 的 src/Domain/Events/ 目錄底下,光是事件類別就有 65 個——ArticleArchiveedEvent、ArticleBackfilledEvent、ArticleBoostedEvent、ArticleCategorizeedEvent、ArticleCloneedEvent……一路到 ArticleWatchedEvent,對應著封存、備份、加權、分類、複製、觀看等等從未被任何業務規則要求過的假設性功能。src/Domain/Listeners/ 底下也對應著幾乎相同數量的 Notify*Listener。

但實際上,PublishArticleService 的建構子裡,EventDispatcher 只掛了一個監聽器:

$dispatcher = new EventDispatcher([new LoggingEventListener($logger)]);

EventDispatcher 本身的實作也很單純:

class EventDispatcher implements EventDispatcherInterface
{
    public function __construct(private array $listeners = [])
    {
    }

    public function dispatch(DomainEventInterface $event): void
    {
        foreach ($this->listeners as $listener) {
            $listener->handle($event);
        }
    }
}

而唯一被掛上的 LoggingEventListener,做的事情是:

public function handle(DomainEventInterface $event): void
{
    $this->logger->log(sprintf(
        '[%s] %s occurred',
        $event->occurredAt()->format(DATE_ATOM),
        get_class($event)
    ));
}

也就是說,整個事件系統實際發生的事,只有「文章被建立/發佈/編輯/下架/刪除時,把這件事寫進一個記憶體陣列 logger」。版本 B 自己在 EventDispatcher 的類別註解裡也承認了這一點:

目前系統裡只掛了 LoggingEventListener,架構上卻已經支援「同一個事件掛多個監聽器」——這是為了將來可能新增的通知、統計等副作用預留的擴充點,即使目前完全用不到。

這套機制到底解決了什麼問題

答案是:沒有解決任何現存的問題。PublishArticleService::publish() 呼叫 PublishArticleCommandHandler,後者除了呼叫 Article::publish()、存回 Repository 之外,額外 dispatch 了一個 ArticlePublishedEvent,但這個事件不影響任何測試斷言、不觸發任何真正的副作用(沒有寄信、沒有通知、沒有寫入其他資料表),它存在的唯一效果,是被 LoggingEventListener 記錄下來。

❌ 常見但站不住腳的理由:

「先把事件廣播機制建好,
以後要加通知/統計功能時比較方便」

✅ 更誠實的判斷方式:

現在有沒有任何一條驗收測試,
要求「發佈文章時要觸發某個副作用」?
如果沒有,這個機制現在就不該存在。

這正是 YAGNI(You Aren't Gonna Need It)原則想提醒的:「以後可能會用到」不是動工的理由,「現在真的需要」才是。 版本 B 的事件系統,是把這個原則反過來套用最極端的例子——不只建了一個可以掛多個監聽器的 EventDispatcher,還先把 65 種假設性事件跟對應的監聽器都寫好了。

為什麼「不影響業務結果」本身就是警訊

一般人 review 到「這段程式碼不影響任何業務結果」時,直覺是「至少沒有風險,放著也無妨」。但放在這個系列的脈絡下,這句話該被讀成另一個意思:如果一套機制的存在與否,不會讓任何一個驗收測試從綠燈變紅燈,那麼它的存在本身,就是純粹的維護成本,沒有對應的效益在平衡它。 65 個事件類別、62 個監聽器類別,合計超過一百個檔案,全部只是在等一個永遠不會發生的「未來需求」。

這也呼應這個系列的主題句:AI 沒有發明過度設計,它只是讓過度設計的速度追上了你按下 Enter 的速度;Review 要跟得上,審的就不能再是程式碼本身,而是產生程式碼的規則。 事件系統本身是一個成熟、有效的模式,問題不在模式,而在「有沒有一條規則要求:新增一個事件類別之前,先確認有沒有測試需要它」。

今日思考題

如果你的專案裡也有類似「先把擴充點搭好」的機制(事件系統、Hook、Plugin 介面),你有沒有回頭確認過,實際上有多少比例的擴充點真的被用上?

今日重點回顧

  • 版本 B 有 65 種事件類別跟對應數量的監聽器,但實際運作時只掛了一個 LoggingEventListener
  • EventDispatcher 的設計本身沒有問題,問題是有 60 幾種事件從未被任何測試觸發
  • 「不影響業務結果」不是優點,是純維護成本沒有效益平衡的訊號
  • YAGNI 原則的具體體現:「將來可能用到」不是現在該寫的理由

明日預告

Day 13 會拆解 DTO/Mapper 轉換鏈——為什麼一個從未被呼叫過的轉換層,還是能讓一次簡單的欄位修改,變成要動 8 個檔案的大工程。

老派工程師的心得

事件驅動架構是我自己也很喜歡的一套設計,用對地方確實能讓系統的耦合度降很多。但這次拆解版本 B 讓我意識到一件事:我以前評估「要不要導入某個架構模式」的標準太寬鬆了——常常只問「這個模式適不適合這種情境」,卻沒有先問「現在有沒有一條需求真的逼出這個情境」。AI 把這個習慣的破壞力放大到:它會很有自信地告訴你「這是最佳實踐」,而你如果沒有先建立起「先問需求,再問模式」的紀律,很容易被這種自信說服。


上一篇
Day 11:案例拆解——多出來的 Repository 裝飾器鏈跟 UnitOfWork 在保護什麼
下一篇
Day 13:案例拆解——DTO/Mapper 轉換鏈為什麼讓一次修改要動 8 個檔案
系列文
當 AI 寫得比你讀得快:Code Review 該審什麼 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言