大家有看過以前鄉下那種燒柴的大灶嗎?
圖片截自爆廢公社
記得以前小時候,阿公阿嬤家有這種燒柴的大灶,阿公下田幹活時,阿嬤就去到處撿柴回來堆在院子裡,晚上要洗澡時就燒熱水來用。那時家裡雖然已經有熱水器,但阿嬤還是每天堅持用大灶燒水,問她為什麼不用熱水器就好,「安呢卡省啦!」她總是簡單這麼回答著,直到後來年紀漸漸大了,開始抬不太動大鍋水了,她才慢慢不再堅持,開始用熱水器洗澡。
在用一個「申請獎學金」的範例看完一次完整的指令以後,你應該也會覺得:「蛤!好麻煩喔!」
是的,如果每個指令都這樣設計,的確是會做不少額外的工作。複雜運算就算了,但有時候,前端就只是想跟後端要一些資料來顯示,也要分這麼多層,這令我們不禁擔心,這種 overhead 真的值得嗎?這種情況下,是否有比較「省事」、比較「輕量級」的做法?
有喔!
今天我們就來聊聊「命令查詢職責分離模式」(CQRS - Command Query Responsibility Segregation)。
Ajay Kumar 在 2019 年出的 CQRS 一書中,建議我們把所有對系統的指令,分成不會改變系統狀態的「查詢(Query)」與會改變系統狀態的「命令(Command)」。以我們的教務處網站系統為例,「獎學金列表」就是一種查詢,「申請獎學金」就是一種命令。
Ajay Kumar 告訴我們,查詢指令不會改變系統狀態,而且經常會改動,所以不需要勞煩 Entity 與 Service 出動,只要叫 Controller 直接呼叫 特定的 Repository 去查就好。除了比較簡單以外,因為 Entity 沒有參與流程,所有前端需求的變動,也不會動到系統核心的 Entity 設計。
Uncle Bob 曾提醒我們,要分辨真正的重複與假性重複。什麼意思?不是說重複是萬惡之淵,必除之而後快嗎?事到如今怎麼突然冒出了個什麼假性的重複,是這搞啥鬼?
這時,就得回頭看看我們講的「重複」到底壞在哪裡了。重複之所以討厭,在於一旦兩個相異物件長得一模一樣,當需求有改,兩個得一起改。那如果這兩個物件雖然長一樣,但不只代表不同東西,而且也不會一起改呢?
胡說!哪有這種東西?
有的,在 Clean Architecture 的系統中,同時套用 CQRS 就會。
在 Query 時,因為不用走進 Service,這時 Controller 會跟 Repository 要求一個特製的物件回傳格式,在不失語意的假設下,我們先稱之為 VO。VO 與 Entity 在系統開發初期可能長得很像,譬如撈取獎學金列表時,Controller 很可能需要撈出跟 Entity 一模一樣的東西。
這時,根據 Ajay Kumar 的建議,要忍住,不可以直接拿 Entity 去回傳,因為我們一直強調的,前端畫面變動得很快,如果此時 Controller 回的是 Entity,不但違反了 Clean Architecture 的跨層原則,更糟糕的是,它讓系統的核心 Entity 隨著最外層的 UI 快速變化,但,你知道的, UI 的變化根本不是核心該管的事情,所以,這時要回一個「為這個 Query 客製化的 VO」。
於是重複就出現了。這使我們陷入進退維谷的窘境,這該如何是好?好佳在,Uncle Bob 這時跳出來解救我們了。他告訴我們,VO 與 Entity 長得像,或是不同 Query 的客製化 VO 長得像,是一種「假性重複」,這種重複不只不會致命,你放著不處理,隨著 UI 慢慢改變,這些 VO 自己會各自演化,最終很有可能也會長得不一樣,如果當初硬著頭皮去消重複,反而會阻礙這種自然演化,造成維護上的困難。
於是我們就放心了,既然無害,那就隨它去吧!
謎之聲:「車過橋頭往岡山,船到橋頭至楠梓。」
圖片截自 YouTube
ithelp2021
文中的 VO,看起來是指 DDD 中提到的 Value Object?
某種程度上可以這麼理解,但是因為我們這裡並沒有要討論 DDD 所以我刻意不去定義它
但是,是的,你可以這麼理解 :)
VO繼承Entity,有需要再用介面限制不就可以避免重複了?
當然可以,沒有問題!程式是自由的 :)
只是 CQRS 建議我們在 Query 的過程中不需要進入 DDD 的模型而已。
但是,程式是自由的,您要這麼做當然可以,誰規定一定要做整套的 CQRS 呢?
不過,繼承雖方便,但卻容易造成一些維護與理解上麻煩的地方,所以不管要不要使用 CQRS,在繼承前還是要慎重一點。
例如, VO 是一種 Entity 嗎?如果不是,那其實一般也不適合繼承。
主要是閣下雖提出如何「避免重複」的建議,
但筆者原文中不但沒有要避免重複,
反而在說的當 Query 出現一些「假性重複」不用太擔心,可以不處理無妨 :)
又想了想,大概了解了
如前文條件
這兩個物件雖然長一樣,但不只代表不同東西,而且也不會一起改
那本來就不該用繼承,確實不需要處理
感謝
文中提及
分成會改變系統狀態的「查詢(Query)」與不會改變系統狀態的「命令(Command)」。
這段看來兩個寫反了。