Clean Architecture背后的核心想法其實(shí)很簡(jiǎn)單:“非核心”應(yīng)該依賴(lài)于“核心”撒汉。
怎么算“核心”己单?對(duì)于一個(gè)應(yīng)用來(lái)說(shuō)塞淹,最最核心的當(dāng)然就是業(yè)務(wù)數(shù)據(jù)(及其結(jié)構(gòu))和業(yè)務(wù)邏輯。這些信息應(yīng)該屬于一個(gè)模塊(也就是常說(shuō)的Service層)栖茉,在實(shí)現(xiàn)上篮绿,應(yīng)該自成一體,對(duì)數(shù)據(jù)持有化(用什么數(shù)據(jù)庫(kù)和什么driver)吕漂、用戶(hù)交互(是通過(guò)RESTful接口還是說(shuō)本身是某個(gè)桌面應(yīng)用的一部分)亲配、或者如何與其它服務(wù)模塊如何交互(消息系統(tǒng)?HTTP請(qǐng)求?)等技術(shù)有任何假定和依賴(lài)吼虎。
舉個(gè)例子犬钢,在設(shè)計(jì)業(yè)務(wù)數(shù)據(jù)結(jié)構(gòu)的時(shí)候,比如創(chuàng)建一個(gè)用戶(hù)的結(jié)構(gòu)思灰,常常需要有個(gè)Id
字段玷犹。這時(shí)候,就算我們知道數(shù)據(jù)庫(kù)會(huì)選用MongoDB洒疚,而驅(qū)動(dòng)技術(shù)用的是Mongo Go Driver歹颓,也不能簡(jiǎn)單地把Id
的類(lèi)型做如下定義:
import "go.mongodb.org/mongo-driver/bson/primitive"
type User struct {
Id primitive.ObjectID
...
}
因?yàn)槿绱艘粊?lái),“核心”的業(yè)務(wù)數(shù)據(jù)就對(duì)“非核心”的技術(shù)選擇產(chǎn)生了依賴(lài)油湖。萬(wàn)一技術(shù)選擇發(fā)生變化巍扛,如從MongoDB改為MySQL,或者棄Mongo Go Driver而改選mgo乏德,業(yè)務(wù)層就要做相應(yīng)改動(dòng)撤奸。
對(duì)上面這個(gè)問(wèn)題,可以這么解決:
type UserId string
type User struct {
Id UserId
...
}
相應(yīng)地鹅经,在Controller層應(yīng)該將用戶(hù)傳入的查詢(xún)鍵轉(zhuǎn)換為UserId
類(lèi)型寂呛,在持久層也應(yīng)該將具體技術(shù)實(shí)現(xiàn)的類(lèi)型轉(zhuǎn)換為UserId
類(lèi)型怎诫。且Controller層和持久層之間也不能做任何數(shù)據(jù)類(lèi)型上的假設(shè)瘾晃。
另外一個(gè)類(lèi)似的問(wèn)題就是如何處理異常。不好的做法是不區(qū)分業(yè)務(wù)異常和技術(shù)異常幻妓。所謂業(yè)務(wù)異常蹦误,舉個(gè)例子,當(dāng)通過(guò)Id
來(lái)查詢(xún)用戶(hù)的時(shí)候肉津,也許與之對(duì)應(yīng)的用戶(hù)信息并不存在强胰,這個(gè)可能性就是一個(gè)業(yè)務(wù)異常。而由于和數(shù)據(jù)庫(kù)的網(wǎng)絡(luò)連接中斷妹沙,而發(fā)生的異常偶洋,就屬于技術(shù)異常。業(yè)務(wù)異常通常是要反饋給終端用戶(hù)的距糖,而技術(shù)異常通常是內(nèi)部異常玄窝,其具體信息一般不需要暴露給用戶(hù)。如果是開(kāi)發(fā)RESTful API悍引,那么對(duì)于技術(shù)異常恩脂,簡(jiǎn)單返回500內(nèi)部錯(cuò)誤就行,具體的錯(cuò)誤信息和原因趣斤,不要暴露給客戶(hù)端(要假定我們的API是給不知名的客戶(hù)端使用的)俩块,而是在日志中寫(xiě)明。
在開(kāi)發(fā)業(yè)務(wù)層時(shí),一般來(lái)說(shuō)應(yīng)該考慮服務(wù)中所提供的每個(gè)操作會(huì)產(chǎn)生哪些業(yè)務(wù)異常玉凯,并且為其定義專(zhuān)門(mén)的類(lèi)型:
type NoUserForSuchIdError struct {
Id UserId
}
func (e *NoUserForSuchIdError) Error() string {
return fmt.Sprintf("no matching user found for id %v", e.Id)
}
在業(yè)務(wù)層里势腮,應(yīng)該使用如上的業(yè)務(wù)錯(cuò)誤類(lèi)型,而不是使用其他層(如持久層)的技術(shù)帶來(lái)的錯(cuò)誤類(lèi)型:
func (s *UsersService) FindUserById(id UserId) (User, *NoUserForSuchIdError, error) {
return s.Repo.FindUserById(id)
}
type UsersRepo interface {
FindUserById(id UserId) (User, *NoUserForSuchIdError, error)
}
而在實(shí)現(xiàn)持續(xù)層時(shí)漫仆,我們要把具體技術(shù)里的相對(duì)應(yīng)錯(cuò)誤類(lèi)型轉(zhuǎn)換成我們自己定義的業(yè)務(wù)錯(cuò)誤:
func (r *MongoGoDriverUsersRepo) FindUserById(id UserId) (User, *NoUserForSuchIdError, error) {
u := User{}
e := r.usersCollection.FindOne(context.TODO(), bson.M{"id", id}).Decode(&u)
if e == mongo.ErrNoDocuments {
return User{}, &domain.NoUserForSuchIdError{Id: id}, nil
}
return u, nil, nil
}
而在Controller層嫉鲸,也應(yīng)該辨認(rèn)這種業(yè)務(wù)錯(cuò)誤,相應(yīng)地設(shè)置HTTP status及報(bào)文Body歹啼。
...
u, noUser, e := c.service.FindUserById(id)
if noUser != nil {
w.WriteHeader(http.StatusNoeFound)
_ = json.NewEncoder(w).Encode(newErrorResponse(noUser.Error()))
return
}
if e != nil {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(new500Response())
return
}
_ = json.NewEncoder(w).Encode(newNormalResponse(u))
值得一提的是玄渗,從開(kāi)頭的圖可以看出,Controller層與持久層都是依賴(lài)于業(yè)務(wù)層的狸眼,所以上面的代碼里藤树,它們可以看到并使用定義在業(yè)務(wù)層的數(shù)據(jù)結(jié)構(gòu)和錯(cuò)誤。