本書前言曾抱怨我們總被迫走捷徑,堆積起一座永遠沒機會償還的技術債大山。要防止捷徑,必須先能辨識它。因此本章的目標是提高對若干潛在捷徑的警覺、並討論其影響——有了這些資訊,我們就能辨識並修正無意間的捷徑;或在有正當理由時,有意識地選擇承受某捷徑的後果。
為什麼捷徑就像破窗#
1969 年,心理學家 Philip Zimbardo 做了一項實驗,後來催生出「破窗理論(Broken Windows Theory)」:
- 他的團隊在布朗克斯(Bronx)某社區停了一輛沒有車牌的車,又在被認為「較好」的帕羅奧圖(Palo Alto)停了另一輛,然後觀察。
- 布朗克斯那輛車 24 小時內值錢零件就被拆光,接著路人開始隨機破壞它。
- 帕羅奧圖那輛車一週都沒人碰,於是團隊自己砸破一扇窗。從那刻起,它便遭遇與布朗克斯那輛車相同的命運,在同樣短的時間內被路人摧毀。
- 參與洗劫破壞的人來自各個社會階層,包括平時奉公守法的良好市民。
用作者自己的話總結破窗理論:一旦某物看起來破敗、受損、或普遍無人照管,人腦就會覺得「把它弄得更破敗、更受損」是可以接受的。
這個理論適用於生活許多面向:在破壞行為常見的社區,洗劫一輛無人照管的車的門檻很低;一輛車有破窗後,即使在「好」社區,進一步破壞它的門檻也很低;在凌亂的臥室,把衣服丟地上而非放進衣櫃的門檻很低。套用到寫程式:
- 在低品質的程式庫上工作,再加一段低品質程式碼的門檻很低。
- 在充滿編碼違規的程式庫上工作,再加一個違規的門檻很低。
- 在充滿捷徑的程式庫上工作,再加一條捷徑的門檻很低。
如此看來,許多所謂「遺留」程式庫的品質隨時間嚴重腐蝕,還令人意外嗎?
從乾淨開始的責任#
雖然寫程式感覺不像洗劫汽車,但我們都在無意識中受破窗心理支配。因此從乾淨開始至關重要——盡量少捷徑、少技術債。因為捷徑一旦悄悄滲入,就會像一扇破窗,吸引更多捷徑。
軟體專案往往昂貴且漫長,把破窗擋在門外是我們身為開發者的重大責任。我們甚至可能不是完成專案的人,得由他人接手;對接手者而言,那是一個他們尚無連結感的遺留程式庫,製造破窗的門檻又進一步降低。
當然,有時我們會判斷走捷徑才是務實之舉——也許這部分程式碼對專案整體不那麼重要、也許在做原型、也許出於經濟考量。
對於這種有意識添加的捷徑,應極力記錄下來,例如採用 Michael Nygard 提倡的「架構決策記錄(Architecture Decision Records,ADR)」。這是我們欠未來的自己與後繼者的。若團隊每位成員都知曉這份文件,甚至能降低破窗效應——因為團隊知道這些捷徑是有意識地、為了正當理由而走的。
以下各節各討論一個在本書六角架構風格中可視為捷徑的模式,看它們的影響以及贊成與反對的論點。
在使用案例間共用模型#
第 5 章主張不同使用案例應有不同的輸入與輸出模型。若兩個使用案例共用同一輸入模型,後果是:
SendMoneyUseCase與RevokeActivityUseCase彼此耦合。改動共用的SendMoneyCommand類別,兩個使用案例都受影響——它們在單一職責原則(應稱「單一改變理由原則」)的意義上共享了一個改變理由。共用輸出模型亦然。

Figure 11.1: 在使用案例間共用輸入或輸出模型導致使用案例彼此耦合
共用是否為捷徑,取決於使用案例是否功能耦合:
- 若它們共享某項需求(功能耦合),我們本就希望改動某細節時兩者都受影響——此時共用是有效的。
- 若它們應能各自獨立演進,那共用就是捷徑。此時應從一開始就分開,即使一開始兩者看起來相同、得複製輸入輸出類別。
因此,圍繞相似概念建構多個使用案例時,值得定期自問:「這些使用案例該各自獨立演進嗎?」一旦答案變成「是」,就該分開輸入與輸出模型。
以領域實體作為輸入或輸出模型#
若有 Account 領域實體與 SendMoneyUseCase 輸入 port,我們可能想直接拿實體當作輸入 port 的輸入/輸出模型。如此一來,輸入 port 便依賴領域實體,後果是為 Account 實體新增了一個改變理由。

Figure 11.2: 以領域實體作為使用案例的輸入或輸出模型,使領域實體與使用案例耦合
等等——Account 並不依賴 SendMoneyUseCase(依賴是反向的),輸入 port 怎會成為實體的改變理由?
假設使用案例需要某項目前
Account沒有的帳戶資訊,而這項資訊最終其實不該存在Account、而該存在另一個領域或有界脈絡。但因為它已出現在使用案例介面中,我們會被誘惑直接往Account加個新欄位——這就是改變理由的來源。
判準如下:
- 對於簡單的建立或更新使用案例,介面中用領域實體或許無妨,因為實體正好包含「要持久化其狀態」所需的資訊。
- 一旦使用案例不只是更新資料庫幾個欄位、而是實作更複雜的領域邏輯(可能委派部分邏輯給充血實體),就該為介面使用專屬的輸入與輸出模型,以免使用案例的變動傳播到領域實體。
這個捷徑危險之處在於:許多使用案例一開始只是簡單的建立或更新,卻隨時間長成複雜領域邏輯的怪獸——在「先做最小可行產品、再逐步加複雜度」的敏捷環境中尤其如此。因此若一開始用了領域實體當輸入模型,必須抓準時機,把它換成獨立於領域實體的專屬輸入模型。
略過輸入 Port#
輸出 port 是反轉「應用層與輸出 adapter」依賴所必需的(讓依賴指向內側),但輸入 port 對依賴反轉而言並非必要。我們可以決定讓輸入 adapter 直接存取應用或領域服務,中間不放輸入 port。移除輸入 port 等於移除了輸入 adapter 與應用層之間的一層抽象——而移除抽象層通常感覺不錯。

Figure 11.3: 沒有輸入 port,我們便失去明確標示的領域邏輯進入點
然而輸入 port 定義了應用核心的進入點。一旦移除:
- 我們得更了解應用內部,才能找出「該呼叫哪個服務方法來實作某使用案例」。保留專屬輸入 port,則能一眼辨識應用的進入點,對新進開發者熟悉程式庫尤其友善。
- 失去了輕鬆強制架構的能力。藉由第 12 章的強制手段,我們能確保輸入 adapter 只呼叫輸入 port 而非應用服務,使每個進入點都成為非常有意識的決定——不再會意外呼叫到「本不該被輸入 adapter 呼叫」的服務方法。
若應用夠小、或只有單一輸入 adapter,能不靠輸入 port 就掌握整個控制流,或許可以省略它。但我們有多少次能斷言「一個應用在其整個生命週期都會保持小巧、或永遠只有單一輸入 adapter」?
略過服務#
除了輸入 port,對某些使用案例我們甚至可能想整個略過服務層:讓輸出 adapter 中的 AccountPersistenceAdapter 直接實作輸入 port,取代通常實作輸入 port 的服務。

Figure 11.4: 沒有服務,程式庫中便不再有使用案例的代表物
對簡單 CRUD 使用案例,這很誘人——因為服務通常只是把建立/更新/刪除請求轉發給持久化 adapter、不加任何領域邏輯。與其轉發,不如讓持久化 adapter 直接實作使用案例。
但這麼做的代價:
- 需要在輸入 adapter 與輸出 adapter 之間共用模型(本例是
Account領域實體),因此通常意味著前述「拿領域模型當輸入模型」的問題。- 應用核心中不再有使用案例的代表物。若某 CRUD 使用案例日後變複雜,會誘使人直接把領域邏輯加進輸出 adapter(因為使用案例已實作在那裡),使領域邏輯去中心化、更難尋找與維護。
歸根結柢,為了避免樣板式的「轉發服務」,我們或許仍可為簡單 CRUD 略過服務。但團隊應制定清楚準則:一旦預期某使用案例要做的不只是建立、更新或刪除實體,就立刻引入服務。
這如何幫助我打造可維護的軟體?#
有時從經濟角度看,走捷徑確實合理。本章提供了一些洞見,幫助判斷某捷徑值不值得走。
- 討論顯示:簡單 CRUD 使用案例最容易誘人走捷徑,因為對它們而言實作整套架構像殺雞用牛刀(而且那些捷徑當下不像捷徑)。
- 但既然所有應用都從小起步,團隊約定「使用案例何時長出 CRUD 狀態」至關重要——唯有如此,才能在那一刻把捷徑換成長期更可維護的架構。
- 有些使用案例永遠不會脫離 CRUD 狀態,對它們而言永久保留捷徑或許更務實,因為並不真的帶來維護負擔。
無論如何,都應記錄架構,以及「為何選擇某捷徑」的決定,好讓我們(或後繼者)日後能重新評估。即使捷徑有時可接受,我們仍希望有意識地做出走捷徑的決定——這意味著要先定義一條「正確」的做事方式並加以強制,這樣當有充分理由時,我們才能有意地偏離它。下一章就來看幾種強制架構的方法。