「我們已經把架構規則寫清楚了:業務邏輯一律經過 Service 層,不能讓 Controller 直接處理。這樣 AI 應該不會再亂寫了吧?」
昨天談到,架構邊界的價值在於限制 AI「能改到哪裡」;但邊界本身不會告訴 AI「這件事值不值得被邊界收編」。今天用一個具體案例,看規則立對了、卻被用錯地方時,會長什麼樣子——架構規則制止了一種錯誤(邏輯散落各處),卻換來另一種錯誤(把不需要規則的東西也硬塞進規則裡)。
假設專案裡有一條架構規則:業務邏輯一律要經過 Service 層,Controller 只負責接request、呼叫 Service、回傳結果。 這條規則的初衷很合理——先前的教訓是 Controller 裡塞了太多判斷邏輯,散落各處,改一個規則要改十幾個地方。
現在有個新需求:把使用者的姓名跟暱稱組合成畫面上顯示的名稱,規則是「有暱稱就顯示『暱稱(本名)』,沒有就只顯示本名」。這段邏輯,老實說五行就能寫完:
❌ AI 遵照架構規則,把五行邏輯包裝成三層:
interface DisplayNameFormatterInterface
{
public function format(User $user): string;
}
class DisplayNameFormatter implements DisplayNameFormatterInterface
{
public function format(User $user): string
{
if ($user->nickname !== null && $user->nickname !== '') {
return sprintf('%s(%s)', $user->nickname, $user->name);
}
return $user->name;
}
}
class DisplayNameFormatterFactory
{
public static function create(): DisplayNameFormatterInterface
{
return new DisplayNameFormatter();
}
}
// 呼叫端
$formatter = DisplayNameFormatterFactory::create();
echo $formatter->format($user);
✅ 同樣遵守「邏輯要經過 Service 層」的規則,但沒有過度包裝:
class UserDisplayService
{
public function formatDisplayName(User $user): string
{
if ($user->nickname !== null && $user->nickname !== '') {
return sprintf('%s(%s)', $user->nickname, $user->name);
}
return $user->name;
}
}
// 呼叫端
echo (new UserDisplayService())->formatDisplayName($user);
兩個版本都沒有違反「業務邏輯要經過 Service 層」的規則——第二版一樣把邏輯放進了 Service,Controller 一樣沒有直接處理判斷邏輯。差別在於,第一版多出了一個介面、一個 Factory,卻沒有任何一個地方用得上「可以替換實作」這個介面帶來的能力,也沒有任何一個地方需要 Factory 決定「該建立哪一種實作」。
架構規則對 AI 來說是一種可以被機械套用的模式:「這是業務邏輯」→「業務邏輯要進 Service」→「Service 通常搭配介面,方便將來替換/測試」→「有介面就常常搭配 Factory 決定要 new 哪個實作」。 這一整串推導在很多情境下是對的,問題是 AI 傾向把它當成一個固定公式,套用在任何被規則認定為「業務邏輯」的東西上,而不會回頭問一句:這個功能有沒有替換實作的需求?有沒有多種可能的建立方式需要 Factory 決定?
如果答案都是沒有,那介面跟 Factory 就只是純粹的間接層,除了讓閱讀這段程式碼的人(不管是人還是下一次接手的 AI)多繞兩層路以外,沒有實際的架構效益。架構規則防住的是「邏輯不知道該放哪裡」,防不住「該不該把這段邏輯包裝成可替換的抽象」這個獨立判斷。
這裡容易有一個誤解:「介面讓你能替換實作,聽起來永遠是好事,為什麼不乾脆每個 Service 都先包一層介面?」
差別在於:這個彈性有沒有一個具體、已知或高機率會發生的變化在等著它。 如果這個顯示名稱格式化邏輯,將來真的會因為不同語系(中文姓名習慣 vs 西方姓名習慣)需要不同的組合規則,那介面 + 多個實作 + Factory 決定用哪個,就是提前佈好的合理彈性。但如果只是「理論上以後可能會變」這種沒有具體依據的假設,這層抽象就只是在賭一個可能永遠不會發生的未來,賭輸的成本卻要每個讀這段程式碼的人天天付。
一個簡單能問的問題是:「如果我現在拿掉這個介面,直接呼叫具體類別,會不會讓任何一個已知的需求變得做不到?」 如果答案是不會,這層抽象目前就是多餘的——不代表永遠不能加,而是等到真的出現第二個實作、真的需要替換的那一刻,再加也不遲,而且到那時候你才知道介面該長什麼樣(現在猜的介面形狀,常常猜錯)。
回想你手上系統裡某個「介面只有一個實作、Factory 只會建立一種東西」的地方:如果拿掉這層間接,會不會讓任何一個已知需求變得做不到?如果答案是不會,這層抽象是在因應真實的變化,還是在因應一個「以後可能會需要」的猜測?
明天要換一個角度看架構約束力:依賴注入容器本身,究竟是幫架構邊界加分的工具,還是可能悄悄變成另一層隱藏耦合?
延伸閱讀:本系列另外三個並行系列——《用 AI Agent 重構一套無框架的 legacy PHP 系統》《AI 時代的 TDD》《讓 AI Agent 維護一個 Open Source Project》——會從不同角度處理相關的經驗,有興趣可以一起追。