Golang整潔架構實踐

動圖封面

動圖封面

騰小云導讀

爲了降低系統組件之間的耦合、提升系統的可維護性,一個好的代碼框架顯得尤爲重要。本文將爲大家介紹衆所周知的三種代碼框架,並從三種框架引申出COLA 架構以及作者基於 COLA 架構設計的 Go 語言項目腳手架實踐方案。希望能給廣大開發愛好者帶來幫助和啓發!


看目錄,點收藏

1.爲什麼要有代碼架構

2.好的代碼架構是如何構建的

2.1 整潔架構

2.2 洋蔥架構

2.3 六邊形架構

2.4 COLA架構

3.推薦一種 Go 代碼架構實踐

4.總結

*本文提及的架構主要指項目組織的“代碼架構”,注意與微服務架構等名詞中的服務架構進行區分。

01、爲什麼要有代碼架構

歷史悠久的項目大都會有很多開發人員參與“貢獻”,在沒有好的指導規則約束的情況下,大抵會變成一團亂麻。剪不斷,理還亂,也沒有開發勇士願意去剪去理。被迫接手的開發勇士如果想要增加一個小需求,可能需要花10倍的時間去理順業務邏輯,再花 10 倍的時間去補充測試代碼,實在是低效又痛苦。

這是一個普遍的痛點問題,有無數開發者嘗試過去解決它。這麼多年發展下來,業界自然也誕生了很多軟件架構。大家耳熟能詳的就有六邊形架構(Hexagonal Architecture),洋蔥架構(Onion Architecture),整潔架構(Clean Architecture)等。

這些架構在細節上有所差異,但是核心目標是一致的:致力於實現軟件系統的關注點分離(separation of concerns)。

關注點分離之後的軟件系統都具備如下特徵:

  • 不依賴特定 UI。 UI 可以任意替換,不會影響系統中其他組件。從 Web UI 變成桌面 UI,甚至變成控制檯 UI 都無所謂,業務邏輯不會被影響。
  • 不依賴特定框架。 以 JavaScript 生態舉例,不管是使用 web 框架 koa、express,還是使用桌面應用框架 electron,還是控制檯框架 commander,業務邏輯都不會被影響,被影響的只會是框架接入的那一層。
  • 不依賴特定外部組件。 系統可以任意使用 MySQL、MongoDB或 Neo4j 作爲數據庫,任意使用 Redis、Memcached或 etcd 作爲鍵值存儲等。業務邏輯不會因爲這些外部組件的替換而變化。
  • 容易測試。 核心業務邏輯可以在不需要 UI、不需要數據庫、不需要 Web 服務器等一切外界組件的情況下被測試。這種純粹的代碼邏輯意味着清晰容易的測試。

軟件系統有了這些特徵後,易於測試,更易於維護、更新,大大減輕了軟件開發人員的心理負擔。所以,好的代碼架構值得推崇。

02、好的代碼架構是如何構建的

前文所述的三個架構在理念上是近似的,從下文圖 1 到圖 3 三幅架構圖中也能看出相似的圈層結構。圖中可以看到,越往外層越具體,越往內層越抽象。這也意味着,越往外越有可能發生變化,包括但不限於框架升級、中間件變更、適配新終端等等。

2.1 整潔架構

圖 1 The Clean Architecture, Robert C. Martin

圖 1 整潔架構的同心圓結構中可以看見三條由外向內的黑色箭頭,它表示依賴規則(The Dependency Rule)。依賴規則規定外層的代碼可以依賴內層,但是內層的代碼不可以依賴外層。也就是說內層邏輯不可以依賴任何外層定義的變量、函數、結構體、類、模塊等等代碼實體。假如最外層藍色層“Frameworks & Drivers” DB 處使用了 go 語言的 gorm 三方庫,並定義了 gorm 相關的數據庫結構體及其 tag 等。那麼內層的 Gateways、Use Cases、Entities 等處不可以引用任何外層中 gorm 相關的結構體或方法,甚至不應該感知到 gorm 的存在。

核心層的 Entities 定義表示核心業務規則的核心業務實體。這些實體既可以是帶方法的類,也可以是帶有一堆函數的結構體。但它們必須是高度抽象的,只可以隨着核心業務規則而變化,不可以隨着外層組件的變化而變化。以簡單博客系統舉例的話,此層可以定義 Blog、Comment 等核心業務實體。

type Blog struct {...}
type Comment struct {...}
  • 核心層的外層是應用業務層

應用業務層的 Use Cases 應該包含軟件系統的所有業務邏輯。該層控制所有流向和流出核心層的數據流,並使用核心層的實體及其業務規則來完成業務需求。此層的變更不會影響核心層、更外層的變更,例如開發框架、數據庫、UI 等變化,也不會影響此層。接着博客系統的例子,此層可以定義 BlogManager 接口,並定義其中的 CreateBlog, LeaveComment 等業務邏輯方法。

type BlogManager interface {
    CreateBlog(...) ...
    LeaveComment(...) ...}
  • 應用業務層的外層是接口適配層

接口適配層的 Controllers 將外層輸入的數據格式轉換成內層 Use Cases 和 Entities 方便使用的格式,然後 Presenters,Gateways 再將內層處理結果轉換成外層方便使用的格式,然後再由更外層呈現到 Web、UI 或者寫入到數據庫。假如系統選擇關係型數據庫做爲其持久化方案的話,那麼所有關於 SQL 的處理都應該在此層完成,更內層不需要感知到任何數據庫的存在。

同理,假如系統與外界服務通信,那麼所有有關外界服務數據的轉化都在此層完成,更內層也不需要感知到外界服務的存在。外層通過此層傳遞數據一般通過DTO(Data Transfer Object)或者DO(Data Object)完成。接上文博客系統例子,示例代碼如下:

type BlogDTO struct { // Data Transfer Object
    Content string `json:"..."`
}

// DTO 與 model.Blog 的轉化在此層完成

func CreateBlog(b *model.Blog) { 
 dbClient.Create(&blog{...})
 ...}
  • 接口適配層的外層是處在最外層的框架和驅動層

該層包含具體的框架和依賴工具的細節,例如系統使用的數據庫、Web 框架、消息隊列等等。此層主要幫助外部的框架、工具,和內層進行數據銜接。接博客系統例子,框架和驅動層如果使用 gorm 來操作數據庫,則相關的示例代碼如下:

import "gorm.io/driver/mysql"
import "gorm.io/gorm"

type blog struct { // Data Object
    Content string `gorm:"..."` // 本層的數據庫 ORM 如果替換,此處的 tag 也需要隨之改變
} 
type MySQLClient struct { DB *gorm.DB }
func New(...) { gorm.Open(...) ... }
func Create(...)...

至此,整潔架構圖中的四層已介紹完成。但此圖中的四層結構僅作示意,整潔架構並不要求軟件系統必須按照此四層結構設計。只要軟件系統能保證“由外向內”的依賴規則,系統的層數多少可自由決定。

整體結構與洋蔥架構二者齊名且結構圖相似,都是四層同心圓。

2.2 洋蔥架構

圖 2 Onion Architecture, Jeffrey Palermo

圖 2 中洋蔥架構最核心的 Domain Model 爲組織中核心業務的狀態及其行爲模型,與整潔架構中的 Entities 高度一致。

其外層的 Domain Services 與整潔架構中的 Use Cases 職責相近。更外層的 Application Services 橋接 UI 和 Infrastructue 中的數據庫、文件、外部服務等,與整潔架構中的 Interface Adaptors 功能相同。最邊緣層的 User Interface 與整潔架構中的最外層 UI 部分一致,Infrastructure 則與整潔架構中的 DB, Devices, External Interfaces 作用一致,只有 Tests 部分稍有差異。

同前兩者齊名的六邊形架構,雖然外形不是同心圓,但是結構上還是有很多對應的地方。

2.3 六邊形架構

圖 3 Hexagon Architecture, Andrew Gordon

圖 3 六邊形架構中灰色箭頭表示依賴注入(Dependency Injection),其與整潔架構中的依賴規則(The Dependency Rule)有異曲同工之妙,也限制了整個架構各組件的依賴方向必須是“由外向內”。圖中的各種 Port 和 Adapter 是六邊形架構的重中之重,故該架構別稱 Ports and Adapters。

圖 4 Hexagon Architecture Phase 1, Pablo Martinez

如圖 4 所示,在六邊形架構中,來自驅動邊(Driving Side)的用戶或外部系統輸入指令通過左邊的 Port & Adapter 到達應用系統,處理後,再通過右邊的 Adapter & Port 輸出到被驅動邊(Driven Side)的數據庫和文件等。

Port 是系統的一個與具體實現無關的入口,該入口定義了外界與系統通信的接口(interface)。Port 不關心接口的具體實現,就像 USB 端口允許多種設備通過其與電腦通信,但它不關心設備與電腦之間的照片、視頻等等具體數據是如何編解碼傳輸的。

圖 5 Hexagon Architecture Phase 2, Pablo Martinez

如圖 5 所示,Adapter 負責 Port 定義的接口的技術實現,並通過 Port 發起與應用系統的交互。例如,圖左 Driving Side 的 Adapter 可以是一個 REST 控制器,客戶端通過它與應用系統通信。圖右 Driven Side 的 Adapter 可以是一個數據庫驅動,應用系統的數據通過它寫入數據庫。此圖中可以看到,雖然六邊形架構看上去與整潔架構不那麼相似,但其應用系統核心層的 Domain 、邊緣層的User Interface 和 Infrastructure 與整潔架構中的 Entities 和 Frameworks & Drivers 完全是一一對應的。

再次回到圖 3 的六邊形架構整體圖:

以 Java 生態爲例,Driving Side 的 HTTP Server In Port 可以承接來自 Jetty 或 Servlet 等 Adapter 的請求,其中 Jetty 的請求可以是來自其他服務的調用。既處在 Driving Side 又處在 Driven Sides 中的 Messaging In/Out Port 可以承接來自 RabbitMQ 的事件請求,也可以將 Application Adapters 中生成的數據寫入到 RabbitMQ。Driven Side 的 Store Out Port 可以將 Application Adapters 產生的數據寫入到 MongoDB;HTTP Client Out Port 則可以將 Application Adapters 產生的數據通過 JettyHTTP 發送到外部服務。

其實,不僅國外有優秀的代碼架構,國內也有。

2.4 COLA架構

國內開發者在學習了六邊形架構、洋蔥架構和整潔架構之後,提出了 COLA(Clean Object-oriented and Layered Architecture)架構,其名稱含義爲「整潔的基於面向對象和分層的架構」。它的核心理念與國外三種架構相同,都是提倡以業務爲核心,解耦外部依賴,分離業務複雜度和技術複雜度[4]。整體架構形式如圖 6 所示。

圖 6 COLA 架構, 張建飛

雖然 COLA 架構不再是同心圓或者六邊形的形式,但是還是能明顯看到前文三種架構的影子。Domain 層中 model 對應整潔架構的 Entities、六邊形架構和洋蔥架構中的 Domain Model。Domain 層中 gateway 和 ability 對應整潔架構的 Use Cases、六邊形架構中的 Application Logic以及洋蔥架構中的 Domain Services。App 層則對應整潔架構 Interface Adapters 層中的 Controllers、Gateways和 Presenters。最上方的 Adapter 層和最下方的 Infrastructure 層合起來與整潔架構的邊緣層 Frameworks & Drivers 相對應。

Adapter 層上方的 Driving adater 與 Infrastructure 層下方的 Driven adapter 更是與六邊形架構中的 Driving Side 和 Driven Side 高度相似。

COLA 架構在 Java 生態中落地已久,也爲開發者們提供了 Java 語言的 archetype,可方便地用於 Java 項目腳手架代碼的生成。筆者受其啓發,推出了一種符合 COLA 架構規則的 Go 語言項目腳手架實踐方案。

03、推薦一種 Go 代碼架構實踐

項目目錄結構如下:

├── adapter // Adapter層,適配各種框架及協議的接入,比如:Gin,tRPC,Echo,Fiber 等
├── application // App層,處理Adapter層適配過後與框架、協議等無關的業務邏輯
│ ├── consumer //(可選)處理外部消息,比如來自消息隊列的事件消費
│ ├── dto // App層的數據傳輸對象,外層到達App層的數據,從App層出發到外層的數據都通過DTO傳播
│ ├── executor // 處理請求,包括command和query
│ └── scheduler //(可選)處理定時任務,比如Cron格式的定時Job
├── domain // Domain層,最核心最純粹的業務實體及其規則的抽象定義
│ ├── gateway // 領域網關,model的核心邏輯以Interface形式在此定義,交由Infra層去實現
│ └── model // 領域模型實體
├── infrastructure // Infra層,各種外部依賴,組件的銜接,以及domain/gateway的具體實現
│ ├── cache //(可選)內層所需緩存的實現,可以是Redis,Memcached等
│ ├── client //(可選)各種中間件client的初始化
│ ├── config // 配置實現
│ ├── database //(可選)內層所需持久化的實現,可以是MySQL,MongoDB,Neo4j等
│ ├── distlock //(可選)內層所需分佈式鎖的實現,可以基於Redis,ZooKeeper,etcd等
│ ├── log // 日誌實現,在此接入第三方日誌庫,避免對內層的污染
│ ├── mq //(可選)內層所需消息隊列的實現,可以是Kafka,RabbitMQ,Pulsar等
│ ├── node //(可選)服務節點一致性協調控制實現,可以基於ZooKeeper,etcd等
│ └── rpc //(可選)廣義上第三方服務的訪問實現,可以通過HTTP,gRPC,tRPC等
└── pkg // 各層可共享的公共組件代

由此目錄結構可以看出通過 Adapter 層屏蔽外界框架、協議的差異,Infrastructure 層囊括各種中間件和外部依賴的具體實現,App 層負責組織輸入、輸出, Domain 層可以完全聚焦在最純粹也最不容易變化的核心業務規則上。

按照前文 infrastructure 中目錄結構,各子目錄中文件樣例參考如下:

├── infrastructure
│ ├── cache
│ │ └── redis.go // Redis 實現的緩存
│ ├── client
│ │ ├── kafka.go // 構建 Kafka client
│ │ ├── mysql.go // 構建 MySQL client
│ │ ├── redis.go // 構建 Redis client(cache和distlock中都會用到 Redis,統一在此構建)
│ │ └── zookeeper.go // 構建 ZooKeeper client
│ ├── config
│ │ └── config.go // 配置定義及其解析
│ ├── database
│ │ ├── dataobject.go // 數據庫操作依賴的數據對象
│ │ └── mysql.go // MySQL 實現的數據持久化
│ ├── distlock
│ │ ├── distributed_lock.go // 分佈式鎖接口,在此是因爲domain/gateway中沒有直接需要此接口
│ │ └── redis.go // Redis 實現的分佈式鎖
│ ├── log
│ │ └── log.go // 日誌封裝
│ ├── mq
│ │ ├── dataobject.go // 消息隊列操作依賴的數據對象
│ │ └── kafka.go // Kafka 實現的消息隊列
│ ├── node
│ │ └── zookeeper_client.go // ZooKeeper 實現的一致性協調節點客戶端
│ └── rpc
│ ├── dataapi.go // 第三方服務訪問功能封裝
│ └── dataobject.go // 第三方服務訪問操作依賴的數據對象

再接前文提到的博客系統例子,假設用 Gin 框架搭建博客系統 API 服務的話,架構各層相關目錄內容大致如下:

// Adapter 層 router.go,路由入口
import (
    "mybusiness.com/blog-api/application/executor" // 向內依賴 App 層

    "github.com/gin-gonic/gin"
)

func NewRouter(...) (*gin.Engine, error) {
  r := gin.Default()
  r.GET("/blog/:blog_id", getBlog)
  ...
}

func getBlog(...) ... {
  // b's type: *executor.BlogOperator
  result := b.GetBlog(blogID)
  // c's type: *gin.Context
  c.JSON(..., result)}

如代碼所體現,Gin 框架的內容會被全部限制在 Adapter 層,其他層不會感知到該框架的存在。

// App 層 executor/blog_operator.go
import "mybusiness.com/blog-api/domain/gateway" // 向內依賴 Domain 層

type BlogOperator struct {
  blogManager gateway.BlogManager // 字段 type 是接口類型,通過 Infra 層具體實現進行依賴注入
}

func (b *BlogOperator) GetBlog(...) ... {
    blog, err := b.blogManager.Load(ctx, blogID)
    ...
    return dto.BlogFromModel(...) // 通過 DTO 傳遞數據到外層}

App 層會依賴 Domain 層定義的領域網關,而領域網關接口會由 Infra 層的具體實現注入。外層調用 App 層方法,通過 DTO 傳遞數據,App 層組織好輸入交給 Domain 層處理,再將得到的結果通過 DTO 傳遞到外層。

// Domain 層 gateway/blog_manager.go
import "mybusiness.com/blog-api/domain/model" // 依賴同層的 model

type BlogManager interface { //定義核心業務邏輯的接口方法
  Load(...) ...
  Save(...) ...
  ...
}
 Domain 層是核心層,不會依賴任何外層組件,只能層內依賴。這也保障了 Domain 層的純粹,保障了整個軟件系統的可維護性。
// Infrastructure 層 database/mysql.go
import (
    "mybusiness.com/blog-api/domain/model" // 依賴內層的 model
    "mybusiness.com/blog-api/infrastructure/client" // 依賴同層的 client
)

type MySQLPersistence struct {
  client client.SQLClient // client 中已構建好了所需客戶端,此處不用引入 MySQL, gorm 相關依賴
}

func (p ...) Load(...) ... { // Domain 層 gateway 中接口方法的實現
  record := p.client.FindOne(...)
  return record.ToModel() // 將 DO(數據對象)轉成 Domain 層 model}

Infrastructure 層中接口方法的實現都需要將結果的數據對象轉化成 Domain 層 model 返回,因爲領域網關 gateway 中定義的接口方法的入參、出參只能包含同層的 model,不可以有外層的數據類型。

前文提及的完整調用流程如圖 7 所示。

圖 7 Blog 讀取過程時序示意圖

如圖,外部請求首先抵達 Adapter 層。如果是讀請求,則攜帶簡單參數來調用 App 層;如果是寫請求,則攜帶 DTO 調用 App 層。App 層將收到的DTO轉化成對應的 Model 調用 Domain 層 gateway 相關業務邏輯接口方法。由於系統初始化階段已經完成依賴注入,接口對應的來自 Infra 層的具體實現會處理完成並返回 Model 到 Domain 層,再由 Domain 層返回到 App 層,最終經由 Adapter 層將響應內容呈現給外部。

至此可知,參照 COLA 設計的系統分層架構可以一層一層地將業務請求剝離乾淨,分別處理後再一層一層地組裝好返回到請求方。各層之間互不干擾,職責分明,有效地降低了系統組件之間的耦合,提升了系統的可維護性。

04、總結

無論哪種架構都不會是項目開發的銀彈,也不會有百試百靈的開發方法論。畢竟引入一種架構是有一定複雜度和較高維護成本的,所以開發者需要根據自身項目類型判斷是否需要引入架構:

不建議引入架構的項目類型:

  • 軟件生命週期大概率會小於三個月的

  • 項目維護人員在現在以及可見的將來只有自己的

可以考慮引入架構的項目類型:

  • 軟件生命週期大概率會大於三個月的

  • 項目維護人員多於1人的

強烈建議引入架構的項目類型:

  • 軟件生命週期大概率會大於三年的

  • 項目維護人員多於5人的

參考文獻:

[1] Robert C. Martin, The Clean Architecture, https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html (2012)

[2] Andrew Gordon, Clean Architecture, https://www.andrewgordon.me/posts/Clean-Architecture/ (2021)

[3] Pablo Martinez, Hexagonal Architecture, there are always two sides to every story, https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c (2021)

[4] 張建飛, COLA 4.0:應用架構的最佳實踐, https://blog.csdn.net/significantfrank/article/details/110934799 (2022)

[5] Jeffrey Palermo, The Onion Architecture, https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/ (2008)

以上是本次分享全部內容,歡迎大家在評論區分享交流。如果覺得內容有用,歡迎轉發~

-End-

原創作者|donghui

技術責編|donghui

“如何更好的降低系統組件之間的耦合、提升系統的可維護性”是讓開發者們亙古不變的頭疼問題,除了設計好的代碼架構,容器化技術等也是重要的解耦技術。大家還能想到哪些可以降低系統耦合度,提高系統可維護性的方法呢?

歡迎在評論區聊一聊你的看法。在4月4日前將你的評論記錄截圖,發送給騰訊雲開發者公衆號後臺,可領取騰訊雲「開發者春季限定紅包封面」一個,數量有限先到先得 。我們還將選取點贊量最高的1位朋友,送出騰訊QQ公仔1個。4月4日中午12點開獎。快邀請你的開發者朋友們一起來參與吧!

最近微信改版啦

很多開發者朋友反饋收不到我們更新的文章

大家可以關注並點亮星標

不再錯過小云的知識速遞

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章