Pragmatic Clean Architecture in Go

Pragmatic Clean Architecture in Go

前言 在大型系統或需要長期維護的產品中,導入 DDD(Domain-Driven Design)或 Clean Architecture 幾乎是業界的標準答案,分層清晰、職責明確、可測試性高,這些優點毋庸置疑。 但問題是,不是每個專案都是大型系統。 當你面對的是一個內部工具、side project、或是剛起步的新產品,Clean Architecture 的大全套 Use Case、Port、Adapter、Aggregate、Repository Interface 全放 domain,往往會讓你在還沒寫第一行商業邏輯之前,就先在資料夾結構裡迷路了三個小時,或是讓不熟悉的貢獻者花費大量時間在閱讀架構。 這篇文章想討論的是:在不犧牲可測試性與可維護性的前提下,哪些抽象可以捨棄、哪些值得保留,以及我自己踩過的一些坑。 捨棄什麼 獨立的 Port/Adapter 層 Clean Architecture 中,Use Case 透過明確定義的 input/output port 與外界溝通,搭配 Adapter 負責格式轉換。這在大型系統中確實有其價值,但它帶來的代價是:為了讓每一層都能獨立替換,你需要維護大量的介面與轉換邏輯,而這些轉換邏輯往往只是把 A struct 的欄位複製到 B struct。 在小專案中,這條轉換鏈可以大幅縮短。HTTP handler 本身就可以負責 DTO 的轉換,不需要再多一層 Adapter。 DDD 的 Aggregate Aggregate 是 DDD 中保護業務不變式(invariant)的邊界,透過 Aggregate Root 統一管理相關物件的存取。這個概念本身沒有問題,但在小專案中,如果業務規則還不夠複雜到需要嚴格的不變式保護,過早引入 Aggregate 反而會讓簡單的 CRUD 操作變得冗長。 保留什麼 捨棄了部分複雜度之後,剩下的四層架構已經足以應付絕大多數的小專案需求。 package domain 這裡只放 Domain Model 與 Value Object。 值得特別說明 Domain Model 的定義。它不等同於 DDD 裡的 Domain,不是什麼深奧的設計概念,Domain Model 只是在描述「這個應用程式如何跟外部世界互動、邊界在哪」,也就是你的核心資料結構與它們身上的行為。有些「賣課」的人喜歡把 Domain Model 直接等同於 DDD 的 Domain,實際上它是個更基礎、更普遍的概念,可以參考這支影片的解釋。Domain Model 中不需要也不該出現任何技術細節,例如回傳是否是 JSON 等等,這些 Domain Model 甚至要能讓 PM 與非技術人員也「聽的懂」。 ...

Interface 不是有開就好:從一個 PR 來看抽象化的重要性

前言 最近團隊正在開發一個新產品,其中一個核心功能需要 client 與 server 之間進行即時、雙向的溝通。經過一番技術評估,我們決定採用 WebSocket 來實現這個需求。 身為一個良好習慣的開發團隊,我們在開發初期就導入了依賴注入(Dependency Injection),希望透過界面(Interface)來解耦商業邏輯與具體的實作,這樣不僅能提高程式碼的可測試性,未來在更換底層實作時也能更加輕鬆。 一切聽起來都很美好,直到我在一次 Code Review 中,看到了一段熟悉的程式碼。 一個 PR 的故事 在我們的 Domain Layer,也就是處理核心商業邏輯的地方,我看到同事定義了下面這個 interface: // package/to/domain/service.go // WebSocketService defines the interface for websocket communication. type WebSocketService interface { // StartAndLinsten starts the service and listens for incoming messages. StartAndLinsten(ctx context.Context) error // Send sends a message to the client. Send(ctx context.Context, message any) error // ... other methods } 第一眼看過去,好像沒什麼大問題。有名稱、有方法、也確實是個 interface。然而,當我細看 WebSocketService 這個命名時,總覺得哪裡怪怪的。 於是我在 PR 上留下了這樣的 comment: 這個界面主要是抽象化 client 與 server 間的互動,不應該侷限於 WebSocket 這個 Protocol。假如我們未來要換成使用 socket.io 或是 gRPC stream,是不是連 domain 層的 interface 也要跟著改動? ...