上一章談了分層架構的問題,本章要提出替代方案。我們會先討論兩個 SOLID 原則,再運用它們建構出整潔架構(Clean Architecture)或六角架構(Hexagonal Architecture),解決分層架構的痛點。
SOLID 是五個原則的縮寫:單一職責原則(Single Responsibility Principle)、開放封閉原則(Open-Closed Principle)、Liskov 替換原則(Liskov Substitution Principle)、介面隔離原則(Interface Segregation Principle)、依賴反轉原則(Dependency Inversion Principle)。本章聚焦其中第一與最後一個。
單一職責原則#
幾乎每個開發者都知道(或自認為知道)單一職責原則(Single Responsibility Principle,SRP)。常見的解讀是:
一個元件應該只做一件事,並把它做好。
這是好建議,卻不是 SRP 真正的本意。「只做一件事」只是「單一職責」最直觀的字面解讀,難怪被廣泛誤讀。SRP 真正的定義是:
一個元件應該只有一個改變的理由(only one reason to change)。
換言之,「職責(responsibility)」應理解為「改變的理由(reason to change)」,而非「只做一件事」。或許該把它改名為「單一改變理由原則」。一個元件若只有一個改變理由,它或許剛好只做一件事,但更重要的是它只會因為這一個理由而改變。
若某元件只有一個改變理由,那麼當我們因其他理由修改軟體時,完全不必擔心它——因為我們知道它仍會如預期運作。可惜的是,改變理由很容易透過依賴關係從一個元件「傳染」到其他元件。
考慮元件 A 直接或間接依賴許多其他元件,而元件 E 沒有任何依賴:
- 元件 E 唯一的改變理由,是新需求要求 E 的功能改變。
- 元件 A 卻可能因為它所依賴的任何元件改變而被迫改變。

Figure 3.1: 元件的每個依賴都是可能的改變理由,即使只是遞移依賴(虛線)
許多程式庫隨時間越來越難改、成本越來越高,正是因為違反 SRP:元件不斷累積改變理由,累積到一定程度後,改動一個元件就可能讓另一個元件出錯。
一個關於副作用的故事#
作者曾接手一個由另一家軟體公司建立、長達十年的程式庫。客戶為了降低維護成本、加快新功能開發而更換了開發團隊。理解這段程式碼相當困難,在某處的改動常在他處引發副作用,但團隊靠著大量測試、補上自動化測試與重構撐了過來。
後來客戶要求一個新功能,卻指定用一種對使用者很彆扭的方式實作。作者提議改用更友善、改動更少、成本更低的做法,但這需要修改某個非常核心的元件。客戶拒絕了,理由是:害怕副作用——因為過去前一個團隊改動那個元件時,總會弄壞別的東西。
這是一個「糟糕架構如何讓客戶被迫多付錢」的活生生案例。所幸多數客戶不會配合這種遊戲,所以讓我們改為打造架構良好的軟體。
依賴反轉原則#
在分層架構中,跨層依賴永遠指向下一層。從高層次套用 SRP 會發現:上層比下層有更多改變理由。由於領域層依賴持久化層,持久化層的每次改動都可能要求領域層跟著改。但領域程式碼是應用程式中最重要的程式碼,我們最不希望它因持久化程式碼的變動而被迫改動。
如何擺脫這個依賴?答案是依賴反轉原則(Dependency Inversion Principle,DIP)。與 SRP 不同,DIP 名副其實:
我們可以反轉(invert)程式庫中任何依賴的方向。
以領域與持久化程式碼之間的依賴為例,反轉步驟如下:
- 先把實體(entity)上移到領域層,因為它們代表領域物件,領域程式碼幾乎都圍繞著改變這些實體的狀態。
- 此時持久化層的 repository 依賴位於領域層的 entity,產生了循環依賴。
- 套用 DIP:在領域層為 repository 建立一個介面,讓持久化層的實際 repository 去實作它。
如此一來,依賴方向被反轉成「持久化層依賴領域層」,領域邏輯就從對持久化程式碼的壓迫性依賴中解放出來。

Figure 3.2: 在領域層引入介面以反轉依賴,使持久化層依賴領域層
只有當我們同時掌控依賴兩端的程式碼時,才能反轉依賴。若依賴的是第三方函式庫,由於我們不掌控其程式碼,便無法反轉。
整潔架構#
「整潔架構」一詞由 Robert C. Martin 在其同名著作中提出。他認為在整潔架構中,業務規則在設計上即可測試,且獨立於框架、資料庫、UI 技術與其他外部應用或介面。這意味著領域程式碼不得有任何朝外的依賴;相反地,藉由 DIP,所有依賴都指向領域程式碼。
整潔架構的層以同心圓層層包裹,核心規則是「依賴規則(Dependency Rule)」:所有跨層依賴都必須指向內側。

Figure 3.3: 整潔架構中,所有依賴都指向內側的領域邏輯(來源:Robert C. Martin《Clean Architecture》)
- 核心:領域實體。
- 環繞核心:使用案例(use case)。它們就是我們先前所稱的服務,但更細緻、各自擁有單一職責(單一改變理由),藉此避免過寬服務的問題。
- 外層:支援業務規則的其他元件,例如提供持久化、UI,或對接任何第三方元件的 adapter。
由於領域程式碼對所用的持久化或 UI 框架一無所知,它就不會包含任何框架專屬程式碼,能專注於業務規則。我們因此享有完全的自由來建模領域,例如純粹地套用領域驅動設計(Domain-Driven Design,DDD)。
整潔架構是有代價的。由於領域層與外層完全解耦,我們必須在每一層各自維護一份應用實體的模型。例如使用 ORM 框架時,ORM 通常要求帶有資料庫對映中繼資料的特定 entity 類別;領域層既然不認識持久化層,就不能共用同一份 entity,必須各建一份,並在層間進行對映。
但這是好事!這種解耦正是我們想要的——讓領域程式碼擺脫框架專屬問題。例如 Java Persistence API 要求 ORM 管理的 entity 具備無參數預設建構子,而這正是我們在領域模型中想避免的。第 9 章會討論不同的對映策略,包括「不對映」這種直接接受領域與持久化層耦合的策略。
六角架構#
「六角架構」一詞源自 Alistair Cockburn,問世已久。它套用的原則,正是 Robert C. Martin 後來在整潔架構中以更通用方式描述的那些。
應用核心被畫成一個六邊形,這也是此風格名稱的由來。六邊形的形狀本身沒有意義——畫成八邊形也行。傳說之所以用六邊形取代常見的矩形,是要表達「應用可以有超過四個邊」來連接其他系統或 adapter。

Figure 3.4: 六角架構又稱「埠與配接器」架構,核心為每個配接器提供專屬 port
- 六邊形內:領域實體,以及操作這些實體的使用案例。六邊形沒有朝外的依賴,因此 Martin 的依賴規則成立——所有依賴都指向中心。
- 六邊形外:各種與應用互動的 adapter,例如對接瀏覽器的 Web adapter、對接外部系統的 adapter、對接資料庫的持久化 adapter。
adapter 分為兩類:
- 驅動 adapter(driving,左側):呼叫應用核心,驅動我們的應用。
- 被驅動 adapter(driven,右側):被應用核心呼叫。
為了讓核心與 adapter 溝通,核心提供特定的「埠(port)」:
- 對驅動 adapter,port 可能是由核心中某個使用案例類別實作、由 adapter 呼叫的介面。
- 對被驅動 adapter,port 可能是由 adapter 實作、由核心呼叫的介面。
- 同一個 port 甚至可由多個 adapter 實作,例如一個對接真實外部系統、一個對接測試用的 mock。
六角架構的核心特徵是:應用核心(六邊形)定義並擁有對外的介面(port),adapter 再去配合這個介面。這正是依賴反轉原則在架構層級的應用,因此這種風格也稱為「埠與 adapter(Ports and Adapters)」架構。
如同整潔架構,六角架構也可組織成層:
- 最外層:在應用與外部系統之間轉譯的 adapter。
- 應用層(application layer):結合 port 與使用案例實作,定義應用的介面。
- 領域層:實作業務規則的領域實體。
業務邏輯實作於使用案例類別與實體中。使用案例類別是狹窄的領域服務,各只實作單一使用案例。我們當然可以把多個使用案例組合成較寬的領域服務,但理想上只在它們經常一起使用時才這麼做,以提升可維護性。
我們也可能想引入應用服務(application service):一個協調對使用案例(領域服務)呼叫的服務。它在輸入/輸出 port 與領域服務之間轉譯,把領域服務與外界隔開,並在需要時協調多個領域服務。(此處的「Domain Service」即前述的「Use Case」,只是改用 DDD 的術語。)

Figure 3.5: 運用 DDD 應用服務與領域服務概念的六角架構
由此可見,六邊形內部的程式碼設計可隨我們的需求自由發揮——簡單或精巧皆可,與應用的複雜度與規模相匹配。第 13 章會更深入探討如何管理六邊形內的程式碼。
這如何幫助我打造可維護的軟體?#
無論你稱它整潔架構、六角架構,還是埠與 adapter 架構——核心都是反轉依賴,讓領域程式碼不依賴外部:
- 領域邏輯與持久化、UI 專屬問題解耦,減少整個程式庫的改變理由,而更少的改變理由帶來更好的可維護性。
- 領域程式碼可以依業務問題建模到最佳,持久化與 UI 程式碼則可依各自的問題建模到最佳。
本書接下來會把六角架構風格套用到一個 Web 應用上。下一章先從建立應用的套件結構、並討論依賴注入的角色開始。