最近在工作過程中重構的項目要求使用DDD架構,在網上查詢資料發現教程五花八門,并且大部分內容都是長篇的概念講解,晦澀難懂,筆者看了一些github上入門的使用DDD的GO項目,并結合自己開發中的經驗,談談自己對DDD的理解。
相關項目
https://github.com/takashabe/go-ddd-sample
基本的CURD,代碼簡潔,適合快速上手 DDD
https://github.com/eyazici90/go-ddd
適合理解第一個項目后,有一定基礎
https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example
比較復雜,適合進階
簡要介紹
?
這里借用github老哥的一張圖來說明,那些長篇大論就不敘述了,因為咱們主要是在代碼中應用嘛,直接講解在項目中要怎么寫。
DDD主要分為幾層,interface,application,domain,infrastructure
在DDD中的調用順序是 interface -> application -> domain
可以類比MVC controller -> service -> model
可以發現DDD比MVC多了一個infrastructure層,這里用某張表的CURD來舉例,這里的某張表在domain層中被稱為某個倉儲(Repository),其實就是這張表的CURD操作,只不過你在domain定義的是接口,接口中方法的技術實現在infrastructure定義。
也就是說你的業務規則在domain定義,業務邏輯中與其他系統(db,文件系統,網絡,第三方庫等)的交互寫在infrastructure層。
舉個簡單的例子,對于用戶表(用戶倉儲),在domain層定義接口
internal/domain/user/user.go
// UserRepository 表示用戶倉庫接口
// 由基礎設施層實現
type UserRepository interface {Get(ctx context.Context, id int) (*domain.User, error) // 根據ID獲取用戶GetAll(ctx context.Context) ([]*domain.User, error) // 獲取所有用戶Save(ctx context.Context, user *domain.User) error // 保存用戶
}
要實現接口中定義的方法,就需要涉及到數據庫(這里是Mysql)的CURD操作,?Mysql不屬于本系統,所以方法的實現放在infrastructure
internal/infrastructure/user/user.go
// userRepository 實現 UserRepository 接口
type userRepository struct {conn *sql.DB // 數據庫連接
}// NewUserRepository 返回初始化的 UserRepository 實現
func NewUserRepository(conn *sql.DB) repository.UserRepository {return &userRepository{conn: conn}
}// Get returns domain.User
// Get 根據ID返回用戶對象
func (r *userRepository) Get(ctx context.Context, id int) (*domain.User, error) {row, err := r.queryRow(ctx, "select id, name from users where id=?", id)if err != nil {return nil, err}u := &domain.User{}err = row.Scan(&u.ID, &u.Name)if err != nil {return nil, err}return u, nil
}// GetAll returns list of domain.User
// GetAll 返回所有用戶列表
func (r *userRepository) GetAll(ctx context.Context) ([]*domain.User, error) {rows, err := r.query(ctx, "select id, name from users")if err != nil {return nil, err}defer rows.Close()us := make([]*domain.User, 0)for rows.Next() {u := &domain.User{}err = rows.Scan(&u.ID, &u.Name)if err != nil {return nil, err}us = append(us, u)}return us, nil
}// Save saves domain.User to storage
// Save 將用戶對象保存到存儲
func (r *userRepository) Save(ctx context.Context, u *domain.User) error {stmt, err := r.conn.Prepare("insert into users (name) values (?)")if err != nil {return err}defer stmt.Close()_, err = stmt.ExecContext(ctx, u.Name)return err
}
完成domain接口的定義以及實現后,相當于MVC的model層實現了,那么只需要在上層調用domain的接口就行了
DDD中的interface,application和MVC的controller和service層一樣的,interface用于接收http請求,把參數傳遞到下層,application用于整合業務邏輯,把http接口需要的數據返回
internal/application/user/user.go 整合數據返回給interface層
// UserInteractor 嵌入了domain的用戶CURD接口
type UserInteractor struct {Repository repository.UserRepository
}// GetUser returns user
// GetUser 返回指定ID的用戶
func (i UserInteractor) GetUser(ctx context.Context, id int) (*domain.User, error) {return i.Repository.Get(ctx, id)
}
internal/interface/user/user.go? 拿到數據返回給前端
// Handler 用戶處理器
type Handler struct {UserInteractor *application.UserInteractor
}// getUser 處理獲取單個用戶的請求
func (h Handler) getUser(w http.ResponseWriter, r *http.Request, id int) {ctx := r.Context()user, err := h.UserInteractor.GetUser(ctx, id)if err != nil {Error(w, http.StatusNotFound, err, "failed to get user") // 獲取用戶失敗return}JSON(w, http.StatusOK, user)
}
一個簡單的DDD架構就這樣實現了
有幾點經驗
- domain層的一個領域負責的是相關的業務,也就是對 /domain/xxx 這個目錄,只要與這個領域相關的表都可以定義在該目錄下
- domain除了定義接口,相關的業務邏輯實現也要放在這里,比如對領域中的某些參數的校驗,即在該領域定義的結構體綁定一些校驗方法等,但是與第三方交互的具體技術實現都放在infrastructure
- 對于想采用CQRS的項目,一般是在application層分別定義查詢實例和命令實例,把涉及到查詢的操作都綁定到查詢實例,把涉及到命令的操作都綁定到命令示例,可以讀寫分離,寫操作可以寫主庫,讀操作讀從庫