「先把事件機制建好,以後要加新功能只要多掛一個 Listener」——這句話你可能自己也講過,聽起來完全合理:今天先花一點成本把架構搭好,將來省下大改的成本。
今天要拆的案例會告訴你,這個邏輯有一個經常被忽略的前提:「將來會不會真的用到」跟「現在能不能想像它被用到」,是兩件不同的事。 版本 B 裡的 Event/Listener 機制,就是後者的產物。
版本 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 介面),你有沒有回頭確認過,實際上有多少比例的擴充點真的被用上?
LoggingEventListener
EventDispatcher 的設計本身沒有問題,問題是有 60 幾種事件從未被任何測試觸發Day 13 會拆解 DTO/Mapper 轉換鏈——為什麼一個從未被呼叫過的轉換層,還是能讓一次簡單的欄位修改,變成要動 8 個檔案的大工程。
事件驅動架構是我自己也很喜歡的一套設計,用對地方確實能讓系統的耦合度降很多。但這次拆解版本 B 讓我意識到一件事:我以前評估「要不要導入某個架構模式」的標準太寬鬆了——常常只問「這個模式適不適合這種情境」,卻沒有先問「現在有沒有一條需求真的逼出這個情境」。AI 把這個習慣的破壞力放大到:它會很有自信地告訴你「這是最佳實踐」,而你如果沒有先建立起「先問需求,再問模式」的紀律,很容易被這種自信說服。