我使用Go和gRPC建立了一個微服務,並將程序設計和編程的最佳實踐應用於該項目。 我寫了一系列關於在項目工做中作出的設計決策和取捨的文章,此篇是關於程序設計。html
程序的設計遵循清晰架構(Clean Architecture)¹。 業務邏輯代碼分三層:用例(usecase),域模型(model)和數據服務(dataservice)。git
有三個頂級包「usecase」,「model」和「dataservice」,每層一個。 在每一個頂級包(模型除外)中只有一個以該包命名的文件。 該文件爲每一個包定義了外部世界的接口。 從頂層向下的依賴結構層次是:「usecase」,「dataservice」和「model」。 上層包依賴於較低層的包,依賴關係永遠不會反向。github
「usecase」是應用程序的入口點,本項目大部分業務邏輯都在用例層。 我從這篇文章²中得到了部分業務邏輯思路。 有三個用例「registration」,「listUser」和「listCourse」。 每一個用例都實現了一個業務功能。 用例可能與真實世界的用例不一樣,它們的建立是爲了說明設計理念。 如下是註冊用例的接口:golang
// RegistrationUseCaseInterface is for users to register themselves to an application. It has registration related functions. // ModifyAndUnregisterWithTx() is the one supporting transaction, the other are not. type RegistrationUseCaseInterface interface { // RegisterUser register a user to an application, basically save it to a database. The returned resultUser that has // a Id ( auto generated by database) after persisted RegisterUser(user *model.User) (resultUser *model.User, err error) // UnregisterUser unregister a user from an application by user name, basically removing it from a database. UnregisterUser(username string) error // ModifyUser change user information based on the User.Id passed in. ModifyUser(user *model.User) error // ModifyAndUnregister change user information and then unregister the user based on the User.Id passed in. // It is created to illustrate transaction, no real use. ModifyAndUnregister(user *model.User) error // ModifyAndUnregisterWithTx change user information and then unregister the user based on the User.Id passed in. // It supports transaction // It is created to illustrate transaction, no real use. ModifyAndUnregisterWithTx(user *model.User) error // EnableTx enable transaction support on use case. Need to be included for each use case needs transaction // It replaces the underline database handler to sql.Tx for each data service that used by this use case EnableTxer }
「main」函數將經過此接口調用「用例」,該接口僅依賴於模型層。sql
如下是「registration.go」的部分代碼,它實現了「RegistrationUseCaseInterface」中的功能。 「RegistrationUseCase」是具體的結構。 它有兩個成員「UserDataInterface」和「TxDataInterface」。 「UserDataInterface」可用於調用數據服務層中的方法(例如「UserDataInterface.Insert(user)」)。 「TxDataInterface」用於實現事務。 它們的具體類型由應用程序容器(ApplicationContainer)建立,並經過依賴注入到每一個函數中。 任何用例代碼僅依賴於數據服務接口,並不依賴於數據庫相關代碼(例如,sql.DB或sql.Stmt)。 任何數據庫訪問代碼都經過數據服務接口執行。數據庫
// RegistrationUseCase implements RegistrationUseCaseInterface. // It has UserDataInterface, which can be used to access persistence layer // TxDataInterface is needed to support transaction type RegistrationUseCase struct { UserDataInterface dataservice.UserDataInterface TxDataInterface dataservice.TxDataInterface } func (ruc *RegistrationUseCase) RegisterUser(user *model.User) (*model.User, error) { err := user.Validate() if err != nil { return nil, errors.Wrap(err, "user validation failed") } isDup, err := ruc.isDuplicate(user.Name) if err != nil { return nil, errors.Wrap(err, "") } if isDup { return nil, errors.New("duplicate user for " + user.Name) } resultUser, err := ruc.UserDataInterface.Insert(user) if err != nil { return nil, errors.Wrap(err, "") } return resultUser, nil }
一般一個用例能夠具備一個或多個功能。 上面的代碼顯示了「RegisterUser」功能。 它首先檢查傳入的參數「user」是否有效,而後檢查用戶是否還沒有註冊,最後調用數據服務層註冊用戶。編程
此層中的代碼負責直接數據庫訪問。 這是域模型「User」的數據持久層的接口。json
// UserDataInterface represents interface for user data access through database type UserDataInterface interface { // Remove deletes a user by user name from database. Remove(username string) (rowsAffected int64, err error) // Find retrieves a user from database based on a user's id Find(id int) (*model.User, error) // FindByName retrieves a user from database by User.Name FindByName(name string) (user *model.User, err error) // FindAll retrieves all users from database as an array of user FindAll() ([]model.User, error) // Update changes user information on the User.Id passed in. Update(user *model.User) (rowsAffected int64, err error) // Insert adds a user to a database. The returned resultUser has a Id, which is auto generated by database Insert(user *model.User) (resultUser *model.User, err error) // Need to add this for transaction support EnableTxer }
如下是「UserDataInterface」中MySql實現「insert」功能的代碼。 這裏我使用「gdbc.SqlGdbc」接口做爲數據庫處理程序的封裝以支持事務。 「gdbc.SqlGdbc」接口的具體實現能夠是sql.DB(不支持事務)或sql.Tx(支持事務)。 經過「UserDataSql」結構傳入函數做爲接收者,使「Insert()」函數對事務變得透明。 在「insert」函數中,它首先從「UserDataSql」獲取數據庫連接,而後建立預處理語句(Prepared statement)並執行它; 最後它獲取插入的id並將其返回給調用函數。服務器
// UserDataSql is the SQL implementation of UserDataInterface type UserDataSql struct { DB gdbc.SqlGdbc } func (uds *UserDataSql) Insert(user *model.User) (*model.User, error) { stmt, err := uds.DB.Prepare(INSERT_USER) if err != nil { return nil, errors.Wrap(err, "") } defer stmt.Close() res, err := stmt.Exec(user.Name, user.Department, user.Created) if err != nil { return nil, errors.Wrap(err, "") } id, err := res.LastInsertId() if err != nil { return nil, errors.Wrap(err, "") } user.Id = int(id) logger.Log.Debug("user inserted:", user) return user, nil }
若是須要支持不一樣的數據庫,則每一個數據庫都須要一個單獨的實現。 我將在另外一篇文章「事務管理³中會詳細解釋。數據結構
模型是惟一沒有接口的程序層。 在Clean Architecture中,它被稱爲「實體(Entity)」。 這是我偏離清晰架構的地方。 此應用程序中的模型層沒有太多業務邏輯,它只定義數據。 大多數業務邏輯都在「用例」層中。 根據個人經驗,因爲延遲加載或其餘緣由,在執行用例時,大多數狀況下域模型中的數據未徹底加載,所以「用例」須要調用數據服務 從數據庫加載數據。 因爲域模型不能調用數據服務,所以業務邏輯必須是在「用例」層。
import ( "github.com/go-ozzo/ozzo-validation" "time" ) // User has a name, department and created date. Name and created are required, department is optional. // Id is auto-generated by database after the user is persisted. // json is for couchdb type User struct { Id int `json:"uid"` Name string `json:"username"` Department string `json:"department"` Created time.Time `json:"created"` } // Validate validates a newly created user, which has not persisted to database yet, so Id is empty func (u User) Validate() error { return validation.ValidateStruct(&u, validation.Field(&u.Name, validation.Required), validation.Field(&u.Created, validation.Required)) } //ValidatePersisted validate a user that has been persisted to database, basically Id is not empty func (u User) ValidatePersisted() error { return validation.ValidateStruct(&u, validation.Field(&u.Id, validation.Required), validation.Field(&u.Name, validation.Required), validation.Field(&u.Created, validation.Required)) }
以上是域模型「User」的代碼,其中有簡單的數據校驗。將校驗邏輯放在模型層中是很天然的,模型層應該是應用程序中的最低層,由於其餘層都依賴它。校驗規則一般只涉及低級別操做,所以不該致使任何依賴問題。此應用程序中使用的校驗庫是ozzo-validation⁴。它是基於接口的,減小了對代碼的干擾。請參閱GoLang中的輸入驗證⁵來比較不一樣的校驗庫。一個問題是「ozzo」依賴於「database/sql」包,由於支持SQL校驗,這搞砸了依賴關係。未來若是出現依賴問題,咱們可能須要切換到不一樣的庫或刪除庫中的「sql」依賴項。
你可能會問爲何要將校驗邏輯放在域模型層中,而將業務邏輯放在「用例」層中?由於業務邏輯一般涉及多個域模型或一個模型的多個實例。例如,產品價格的計算取決於購買數量以及商品是否在甩賣,所以必須在「用例」層中。另外一方面,校驗邏輯一般依賴於模型的一個實例,所以能夠將其放入模型中。若是校驗涉及多個模型或模型的多個實例(例如檢查用戶是否重複註冊),則將其放在「用例」層中。
這是我沒有遵循清晰架構(Clean Architecture)的另外一項。 根據清晰架構(Clean Architecture)¹,「一般跨越邊界的數據是簡單的數據結構。 若是你願意,可使用基本結構或簡單的數據傳輸對象(DTO)。「在本程序中不使用DTO(數據傳輸對象),而使用域模型進行跨越邊界的數據傳輸。 若是業務邏輯很是複雜,那麼擁有一個單獨的DTO可能會有一些好處,那時我不介意建立它們,但如今不須要。
跨越服務邊界時,咱們確實須要擁有不一樣的域模型。 例如本應用程序也做爲gRPC微服務發佈。 在服務器端,咱們使用本程序域模型; 在客戶端,咱們使用gRPC域模型,它們的類型是不一樣的,所以須要進行格式轉換。
// GrpcToUser converts from grpc User type to domain Model user type func GrpcToUser(user *uspb.User) (*model.User, error) { if user == nil { return nil, nil } resultUser := model.User{} resultUser.Id = int(user.Id) resultUser.Name = user.Name resultUser.Department = user.Department created, err := ptypes.Timestamp(user.Created) if err != nil { return nil, errors.Wrap(err, "") } resultUser.Created = created return &resultUser, nil } // UserToGrpc converts from domain Model User type to grpc user type func UserToGrpc(user *model.User) (*uspb.User, error) { if user == nil { return nil, nil } resultUser := uspb.User{} resultUser.Id = int32(user.Id) resultUser.Name = user.Name resultUser.Department = user.Department created, err := ptypes.TimestampProto(user.Created) if err != nil { return nil, errors.Wrap(err, "") } resultUser.Created = created return &resultUser, nil } // UserListToGrpc converts from array of domain Model User type to array of grpc user type func UserListToGrpc(ul []model.User) ([]*uspb.User, error) { var gul []*uspb.User for _, user := range ul { gu, err := UserToGrpc(&user) if err != nil { return nil, errors.Wrap(err, "") } gul = append(gul, gu) } return gul, nil }
上述數據轉換代碼位於「adapter/userclient」包中。 乍一看,彷佛應該讓域模型「User」具備方法「toGrpc()」,它將像這樣執行 - 「user.toGrpc(user * uspb.User)」,但這將使業務域模型依賴於gRPC。 所以,最好建立一個單獨的函數並將其放在「adapter/userclient」包中。 該包將依賴於域模型和gRPC模型。 正由於如此,保證了域模型和gRPC模型都是乾淨的,它們並不相互依賴。
本應用程序的設計遵循清晰架構(Clean Architecture)。 業務邏輯代碼有三層:「用例」,「域模型」和「數據服務」。 可是我在兩個方面偏離了清晰架構(Clean Architecture)。 一個是我把大多數業務邏輯代碼放在「用例」層; 另外一個是我沒有數據傳輸對象(DTO),而是使用域模型在不一樣層之間進行共享數據。
完整的源程序連接 github。
[3] Go Microservice with Clean Architecture: Transaction Support
[5] Input validation in GoLang
不堆砌術語,不羅列架構,不迷信權威,不盲從流行,堅持獨立思考