領域驅動最佳實踐--用代碼來告訴你來如何進行領域驅動設計

作一個租戶系統下的權限服務,接管用戶的認證和受權,咱們取名該服務爲go-easy-logingit

     本文實質是領域驅動設計之實戰權限系統微服務的進一步總結和改進,學習領域驅動設計自己是按部就班的過程,培養的是領域的概念和麪向對象編程思想,而過去以及如今,包括將來,多數人只是披着面向對象的皮,幹着面向過程,面向數據庫的糙活,詳情請看爲何咱們須要領域驅動設計,若是你接觸過領域驅動設計,可是苦於不知道如何動手,概念雖懂但不知如何實踐,本篇將能爲你打開實踐領域驅動設計的大門,若是你不曾瞭解過領域驅動設計,這篇一樣也是入門領域驅動設計的最好文章之一,帶你感覺領域驅動的非凡魅力。github

項目結構

     代碼先行,先展現一下代碼的目錄結構以及相應的文件,你們能夠先YY對應的做用,而後帶着疑問去閱讀。數據庫

login
  	base
                encrypt.go
  		token.go
  		repoImpl.go
      domain
	  	   service
  			loginService.go
  			loginService_test.go
  		  loginUser.go
  		  loginUser_test.go
  	mocks
  		  EncryptHelper.go
  		  LoginUserRepo.go
複製代碼

如何脫離技術細節

     領域驅動設計更增強調業務邏輯以及相應對創建起的領域(模型),不該該出現任何 技術細節,即數據庫,緩存等。面向對象是對於外在客觀事物的一個模擬和反映,一 個User類應該具有eat,drink,play,happy等能力,一個User不可能具有鏈接數據庫的能力,出現依賴任何技術細節是違反面向對象編程的。那麼問題來了,道理我都懂,如何去作到,若是咱們在一個項目中,什麼技術都不用到的話,是否是就達到咱們的目的了?(讀者疑問:WTF,怎麼可能?)編程

     新建一個項目,什麼第三方包都不依賴,根據咱們想作的功能,作一個租戶系統下的權限服務,接管用戶的認證和受權,咱們新建一個LoginUserE來表明登錄用戶,DoVerify執行認證過程,同時咱們但願具有賬密登錄的時候,由調用系統決定加密方式,這就意味着LoginUserE這個領域須要能夠根據EncryptWay來獲取EncryptHelper,咱們先新建一個loginUser.go緩存

package domain

type LoginUserE struct {
	Username     string
	IsLock       bool
	UniqueCode   string
	Mobile       string
	canLoginFunc func() bool EncryptWay } func (user *LoginUserE) CanLogin() bool {
	var can bool
	if user.canLoginFunc != nil {
		can = user.canLoginFunc()
	} else {
		can = !user.IsLock
	}
	return can
}

func (user *LoginUserE) DoVerify(sourceCode string, encryptedCode string) (bool, error) {
	if !user.CanLogin() {
		return false, errors.New("can not login")
	}
	match := user.EncryptHelper().Match(sourceCode, encryptedCode)
	return match, nil
}
複製代碼

     這裏的問題在於EncryptHelper()這個方法,咱們知道加密方法,就拿MD5來講,必須須要依賴到其餘包,而loginUser.go咱們是不但願依賴到任何第三方包的,這彷佛進入了一種矛盾。Alistair Cockburn 提出的六邊形架構,在於domain處於核心內部,其餘的依賴經過接口進行交流,再換句話說就是domain層定義接口,基礎設施層(技術層)實現接口,咱們定義EncryptHelper接口架構

type EncryptHelper interface {
	Encrypt(password string) string
	Decrypt(password string) string
	Match(source, encryptedString string) bool
}
複製代碼

     而後在base基礎設施層新建encrypt.go實現該類app

type MD5Way struct{}

func (md5 MD5Way) Match(source, encryptedString string) bool {
	return md5.Encrypt(source) == encryptedString
}

func (MD5Way) Encrypt(password string) string {
	data := []byte(password)
	md5Bytes := md5.Sum(data)
	return string(md5Bytes[:])
}

func (MD5Way) Decrypt(password string) string {
	panic("not support")
}
複製代碼

      問題還沒可以解決,base層的具體實現類,如何讓domain層中不直接依賴的同時,又能使用呢?最好的方法其實是依賴注入,可是引入依賴注入又陷入另外一種悖論--不依賴任何技術細節,依賴注入也能夠概括爲技術的一種,下文再繼續探討這點,且看我如何不用依賴注入實現。在loginUser.go咱們新建一個全局變量var EncryptMap = make(map[EncryptWay]EncryptHelper)框架

var EncryptMap = make(map[EncryptWay]EncryptHelper)

func (encryptWay EncryptWay) EncryptHelper() EncryptHelper {
	if helper, ok := EncryptMap[encryptWay]; ok {
		return helper
	} else {
		panic("can not find helper")
	}
}

func AddEncryptHelper(encryptWay EncryptWay, helper EncryptHelper) {
	EncryptMap[encryptWay] = helper
}
複製代碼

     核心層以外的類,經過AddEncryptHelper註冊相應的EncryptHelper,這種設計初看尚可,可是一旦項目中具有更多個領域,再採用這種方法則會致使代碼的維護成本的提升,依賴注入實則是屏蔽構建具體實現類的過程,要不要在domain層引入,因人而異,因項目而異。如有更好的方法,歡迎在評論區中提出。dom

領域服務

     若你是初涉或者從未涉及過領域驅動設計,你的思惟會比較固定,如爲何咱們須要領域驅動設計,長期以來你習慣以數據表爲核心進行分析設計,想着某個功能咱們應該如何建表。我敢打包票,在上面講解的功能過程當中,你一開始就在思考這個表怎麼設計的,我從表中取哪一個字段再怎樣怎樣,這是絕對的被數據庫,被技術綁架了的思惟可是,問題也在於,咱們最終老是須要解決數據庫這個問題,這個問題也就是在領域服務中解決。 領域服務解決的另外一個問題是組裝邏輯,舉個例子,LoginUserE.DoVerify雖然不依賴任何第三方包或者同級的其餘類,可是他的入參被咱們依賴隔離,這個入參可能就依賴其餘domain,所以,咱們須要領域服務去組裝這一層。 咱們來說一下登錄是如何實現的,首先咱們定義LoginCmdLogin的入參,函數

//implemention will show right behind this
func (service *LoginService) Login(loginCmd common.LoginCmd) (string, error) type LoginCmd struct {
	Username         string
	TenantId         string
	EffectiveSeconds int
	Mobile           string
	SourceCode       string
	LoginWay         string
	EncryptWay       string
}
複製代碼

     這裏就到了設計數據庫的地方,咱們須要去查找判斷這個用戶是否存在,那麼問題來了,咱們不能直接依賴數據庫技術,可是咱們又須要,這可咋整?相似的固然是定義接口

type LoginUserRepo interface {
	GetOne(username, tenantId string) *domain.LoginUserDO
}
複製代碼

可是這裏又回到了上文討論依賴注入的地方了,這裏我爲了簡單起見,仍然沒有用到依賴注入,可是我我的是建議使用的

var loginService *LoginService
type LoginService struct {
	LoginUserRepo
}
func NewLoginService(repo LoginUserRepo,) *LoginService {
//do not argue to use double check lock,it's a example and does not hurt anyway
	if loginService == nil {
		return &LoginService{
			LoginUserRepo: repo,
		}
	} else {
		return loginService
	}
}
複製代碼

     這樣咱們就在初始化LoginService的時候將repoImpl傳送進行,達到了依賴隔離的目的。領域服務不須要知道任何倉儲手段,甚者無需知道底層用的是什麼數據庫,我只關心取和拿,我只要結果,定義接口的實質目的也在於此。

      回到GetOne(username, tenantId string) *domain.LoginUserDO這個方法,這裏還暴露了一個點在於,DataObject類是定義在domain層中,而不是在service,更不是在base中,我之前糾結的一點是,既然domain層不依賴數據庫技術,是否是也應該不關心DataObject,DataObject是否是放在base層下更加合適?

     如今之因此把DataObject放在domain層,緣由在於

1.domain核心層不直接依賴其餘層,若是DataObject放在base層勢必違背這點;
2.domain層做爲接口定義者,有權根據他自身的需求定義他想要的存儲內容,其餘層只須要服從而且實現。
複製代碼

     同時,咱們不但願代碼中充斥着大量的convert,從cmd轉到DO,從DO轉到E,因此咱們提煉出了dto.go這個文件,用於存放concert代碼。最終的代碼形式以下。

func (service *LoginService) Login(loginCmd common.LoginCmd) (string, error) {
	userDO := service.GetOne(loginCmd.Username, loginCmd.TenantId)
	userE := common.ToLoginUserE(*userDO)
	userE.EncryptWay = domain.EncryptWay(loginCmd.EncryptWay)

//login way contains PASSWORD and SMS ,encryptCode()is to get which one to be verify ,so userE will not to care about which way is exactly by logining
	encryptCode := service.encryptCode(loginCmd.LoginWay, userDO)
	if _, err := userE.DoVerify(loginCmd.SourceCode, encryptCode); err != nil {
		return "", err
	}

	//todo add login event and callback
	return service.token(userE.UniqueCode, loginCmd.EffectiveSeconds), nil
}

func (service *LoginService) encryptCode(way string, userDO *domain.LoginUserDO) string {
	switch way {
	case "PASSWORD":
		return userDO.Password
	case "SMS":
		return service.FindSmsCode(userDO.Mobile)
	default:
		panic("unknown login way")
	}
}
複製代碼

      新的風暴又出現了,service.token(userE.UniqueCode, loginCmd.EffectiveSeconds)這段邏輯是什麼意思,上文中也沒有出現,待我慢慢需講解。正常登錄下咱們校驗成功以後須要授予token,可是token的生成技術細節,用JWT仍是什麼其餘的,domain不該該關心,因此咱們給loginService 加一個類型爲函數的field

type LoginService struct {
	LoginUserRepo
	token func(uniqueCode string, effectiveSeconds int) string } func NewLoginService(repo LoginUserRepo, token func(uniqueCode string, effectiveSeconds int) string) *LoginService {
	if loginService == nil {
		return &LoginService{
			LoginUserRepo: repo,
			token:         token,
		}
	} else {
		return loginService
	}
}
複製代碼

最終效果

     拷貝不走樣,屏蔽技術細節,強調業務邏輯,最終目的是實現業務邏輯可重用,組織爲一個可重用的自封閉的業務模型。最終咱們很好的構建了這樣的一個模型。

     這個業務模型不管置身於任何技術框架,任何Web框架,仍是其餘的場景,都不會受到破壞,不管選擇任何數據庫技術,也不會影響到這個模型。外在技術的細節這裏就不跟着你們一塊兒實現了,本篇文章重在構建模型,技術的選擇就由本身去作決定,這也絲毫影響不了模型。

測試驅動使領域驅動更加完美

     全篇下來的奧義在於隔離依賴,這些都是經驗積累,有沒有行之有效的規範得以遵照,答案是我也不知道,可是若是你遵照測試驅動的行爲的話,這會迫使你去思考,什麼該依賴,什麼不應依賴,由於全部的第三方依賴,都須要用Mock去代替,這就是爲何目錄中存在mocks這個文件。測試寫得好,煩惱多不了。

總結

     回頭看這是我寫的第三篇涉及領域驅動設計的文章,目的在於可以讓更多的人更加容易理解而且實踐領域驅動設計,寫出優秀的代碼,得出接任者的稱讚,提升代碼質量。

     路漫漫其修遠兮,看官點個讚唄!

     做者:plz叫我紅領巾

     出處:領域驅動最佳實踐--用代碼來告訴你來如何進行領域驅動設計

    源碼地址:github.com/iamlufy/go-… 代碼會逐步完善,能夠看看git提交記錄~

    本博客歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。

相關文章
相關標籤/搜索