微服務業務架構的探索
原文:微服務業務架構的探索
提示
閱讀這篇文章,需要有以下準備:
- 在微服務下掙扎過
- 需要了解 DDD 和COLA架構思想
- 本篇文章圍繞業務架構進行討論
前言
公司在開始探索微服務架構時,使用的是三層架構(controller/service/model)。隨著時間的推移,發現三層業務架構在微服務架構下越來越不適用,主要體現在下面 2 點:
- 業務邏輯離散在 service 層,不能很好的複用和表達能力差
- 業務程式碼和技術實現進行了強耦合,導致除錯和測試困難 針對以上問題,我們開始探索新的業務架構,整理形成我們自己研發的業務框架:Esim (Make everything simple)。
回顧
我對業務架構的思考,來自一道比較經典的面試題:什麼是 MVC?估計剛畢業的同學,都避免不了這道面試題。當然時間總是飛逝的,從畢業到現在,經歷了 PC 時代,移動時代,到現在的微服務時代的技術變遷。技術的層出不窮,讓我應接不暇。在回顧這個變遷的過程中發現一些比較有趣的事情,所以拿出來分享:
-
架構一直在演進
之所以用 “演進” 這個詞,是因為新的架構思想需要一步一步形成,換句話說需要時間。我們以三層架構開始探索微服務,用了 2 年多,因為越來越痛苦才開始探索新的業務架構,但也花了 1 年多的時間,才有一個成形的框架。
-
都是圍繞模型,行為,資料進行變化
自從把資料,模型,行為 3 兄弟從大雜燴解放出來後,他們就一直纏著你,這種糾纏很有可能伴隨你的整個職業生涯。在 PC 時代,我們把 3 兄弟放到 model 裡,所以當時有
胖M,廋C
的說法,有了經驗後,在移動時代,我們把行為抽出來放到service
,model 留下資料和模型,再到現在的微服務時代,我們把行為和模型放到domain
,資料放到了infrastructure
。整個演進過程都圍繞著這 3 兄弟。 -
邊界明顯
不同時代的的架構邊界很清晰,PC 時代說的是職責分離,移動時代說的是前後端分離,微服務時代說的是業務邏輯和技術分離。這些邊界的出現和當時的環境脫不了關係。
事務指令碼到領域模型
- 什麼是事務指令碼
使用過程來組織業務邏輯,每個過程處理來自表現層的請求。
事務指令碼勝在簡單,也正是簡單,身邊的很多同事也在使用相同的方式來組織程式碼,我自己也沉浸在裡面很長時間,沒有思考是否有更好的方式(需要吸取這個教訓)。
- 什麼是領域模型
在領域中合併了資料和行為的物件模型
領域模型強調的是組織業務邏輯前,先關注物件的行為,而事務指令碼關注資料。
- 例子
以我們最近重構的紅包業務邏輯舉個例看看他們之間的區別:只能在指定的洗車業務和 A 商家才能使用該紅包。
事務指令碼實現
couponService.go
//是否滿足紅包使用條件
func (cs CouponService) IsSatisfyUse(couponId int, bussinessType string, sellerId string) bool {
couponInfo := cs.CouponDao.FindById(couponId)
......
couponConfInfo := cs.CouponConfigDao.FindById(couponInfo.ConfigId)
......
//空代表所有業務都可以
if couponConfInfo.allowBussiness == "" {
return true
}
if bussinessType == "" {
return false
}
var allowBussiness bool
allowBussinesses := strings.Split(couponConfInfo.allowBussiness, ",")
for _, val := range allowBussinesses {
if bussinessType == val {
allowBussiness = true
}
}
if inBusiness == false {
return false
}
//空所有商家都允許使用
if couponConfInfo.allowSellers == "" {
return true
}
if sellerId == "" {
return false
}
var allowSeller bool
allowSellers := strings.Split(couponConfInfo.allowSellers, ",")
for _, seller := range allowSellers {
if sellerId == seller {
allowSeller = true
}
}
if allowSeller == true {
return true
} else {
return false
}
}
上面的程式碼就是比較典型的” 一杆到底 “,這樣形式的程式碼在我們的系統很常見。 經常導致業務邏輯的程式碼不能很好的複用,業務邏輯分散在多個不同的方法或 service 檔案裡,很少有人能把他們慢慢找出來, 封裝成共用方法。即使找到了又不敢輕易的把它們提取出來,因為它有可能和其他業務邏輯已經綁在了一起。
當你抱著提升程式碼質量的情懷把它們提取出來,又因為沒有很好的方法驗證是否會影響了原有的業務邏輯。 導致出了很多次和原來預期對不上的問題(當時個個都堅信不會出問題),也讓很多同學對自己產生了懷疑。 所以為了避免這些問題發生,我們通常對這些能複用的程式碼睜一隻眼閉一眼,包括我自己。
領域模型的實現
coupon_service.go
//是否滿足紅包使用條件
func (cs CouponService) IsSatisfyUse(couponId int, bussinessType string, sellerId string) bool {
couponInfo := cs.CouponDao.FindById(couponId)
......
couponConfInfo := cs.CouponConfigDao.FindById(couponInfo.ConfigId)
......
if couponConfInfo.CheckAllowBusiness(bussinessType) == false {
return false
}
if couponConfInfo.CheckAllowSeller(sellerId) == false {
return false
}
return true
}
entity/coupon_config.go
type CouponConfig struct {
id int
allowBussiness string
allowSellers string
......
}
func (cc CouponConfig) CheckAllowBusiness(bussinessType string) bool {
//所有業務都可以
if cc.allowBussiness == "" {
return true
}
if bussinessType == "" {
return false
}
allowBussinesses := strings.Split(cc.allowBussiness, ",")
for _, val := range allowBussinesses {
if bussinessType == val {
return true
}
}
return false
}
func (cc CouponConfig) CheckAllowSeller(sellerId string) bool {
//所有商家都允許使用
if cc.allowSellers == "" {
return true
}
if sellerId == "" {
return false
}
allowSellers := strings.Split(cc.allowSellers, ",")
for _, seller := range allowSellers {
if sellerId == seller {
return true
}
}
return false
}
從上面的程式碼可以看出,我們把原來在
coupon_service.go
的業務邏輯都放到了實體coupon_config.go
裡面(行為和模型綁在了一起)。 業務邏輯不再離散,更內聚,能很好的複用,且寫單元測試變得簡單。
entity/coupon_config_test.go
func TestEntity_CheckAllowSeller(t *testing.T) {
testCases := []struct{
caseName string
sellerId string
allowSellers string
expected bool
}{
{"允許—空", "100", "", true},
{"允許2", "1", "1,100", true},
{"不允許", "1", "2,3,4", false},
}
for _, test := range testCases{
t.Run(test.caseName, func(t *testing.T) {
cc := CouponConfig{}
cc.allowSellers = test.allowSellers
result := cc.CheckAllowSeller(test.sellerId)
assert.Equal(t, test.expected, result)
})
}
}
領域模型讓我們寫單元測試的時候不再關注所依賴的儲存實現,讓寫單元測試這件事變得輕鬆、簡單。
三層架構 到 四層架構
三層架構和四層架構一個明顯的區別是業務和實現技術分離。
在三層架構,業務和實現技術進行了強耦合,讓開發在除錯和測試時都要依賴真實的服務,導致浪費了很多時間在部署服務,造資料環節上,這個問題在微服務架構下更突出。四層架構可以很好的解決這個問題。還是以上面的程式碼為例(直接依賴了 mysql):
coupon_service.go
//是否滿足紅包使用條件
func (cs CouponService) IsSatisfyUse(couponId int, bussinessType string, sellerId string) bool {
couponInfo := cs.CouponRepo.FindById(couponId)
......
couponConfInfo := cs.CouponConfigDao.FindById(couponInfo.ConfigId)
......
return true
}
三層實現測試IsSatisfyUse
(使用 gorm 的mock SDK):
coupon_service_test.go
func TestCouponRepo_IsSatisfyUse(t *testing.T) {
cs := NewCouponService()
......
couponReply := []map[string]interface{}{{"config_id": "100"}}
couonConfigReply := []map[string]interface{}{{"allow_bussinesses": "1,2", "allow_sellers" : "1,2"}}
Catcher.Attach([]*FakeResponse{
{
Pattern:"SELECT * FROM coupon WHERE",
Response: couponReply,
Once: false,
},
{
Pattern:"SELECT * FROM coupon_config WHERE",
Response: couonConfigReply,
Once: false,
},
})
result := cs.IsSatisfyUse(100, "1", "1")
assert.Equal(t, true, result)
}
上面的程式碼問題在於:如果業務程式碼依賴了某個技術實現,就要用對應的 mock SDK 來寫單元測試。 只依賴一個mysql可能不會有太大問題,但技術發展到現在,業務邏輯基本不可能只依賴 mysql。 還有可能是:redis,mongodb,http,grpc 等,這說明你需要學習各式各樣的 mock SDk。 我當初就被這些海量的 SDK,折騰的異常痛苦。也是這個原因才去尋找更好的辦法:分離業務邏輯和技術實現。
四層實現 IsSatisfyUse
(使用依賴倒置)
coupon_service.go
//是否滿足紅包使用條件
func (cs CouponService) IsSatisfyUse(couponId int, bussinessType string, sellerId string) bool {
couponInfo := cs.CouponRepo.FindById(couponId)
......
couponConfInfo := cs.CouponConfigRepo.FindById(couponInfo.ConfigId)
......
return true
}
infra/repo/coupon_repo.go
//定義介面
type CouponRepo interface {
FindById(int64) entity.Coupon
}
//db實現
type DBCouponRepo struct {
couponDao *dao.CouponDao
}
func (dcr *DBCouponRepo) FindById(id int64) entity.Coupon {
......
coupon, err = dcr.couponDao.Find("*", "id = ? ", id)
......
return coupon
}
//coupon_config 同理
四層實現測試IsSatisfyUse
(使用mockery SDK):
coupon_service_test.go
func TestCouponRepo_IsSatisfyUse(t *testing.T) {
cs := NewCouponService()
......
couponRepo := &mocks.CouponRepo{}
couponRepo.On("FindById", int64(100)).Return(entity.Coupon{ConfigId : 100})
cs.CouponRepo = couponRepo
couponConfigRepo := &mocks.CouponConfigRepo{}
couponConfigRepo.On("FindById", int64(100)).Return(entity.CouponConfig{AllowBussiness : "1", "AllowSellers" : "1"})
cs.CouponConfigRepo = couponConfigRepo
result := cs.IsSatisfyUse(100, "1", "1")
assert.Equal(t, true, result)
}
通過依賴倒置將具體的技術實現和業務分離,你將不再需要學習各式各樣的 mock SDK。 使用這種方式還有其他好處:
- 如果你要從 mysql 切換成其他儲存層,只需要重新實現
CouponRepo
就可以了。不需要改動任何業務邏輯,且TestCouponRepo_IsSatisfyUse
,還能正常使用。- 使用介面分離技術實現,可以讓你在開發過程不用關注依賴的服務是否可用,非常的便利。
結語
領域模型和四層架構可以很好的解決了我們當前存在的問題,但它們也存在其他問題:
-
有一定的學習成本
有學習成本的一個原因是:現在大量的開發都是在使用事務指令碼和三層架構做業務開發,要想轉向領域模型和四層架構, 需要花點時間(他們向工程師提了要求),但是如果轉成功了,將會對公司的業務程式碼在測試性和擴充套件性上有很大的提升。
-
增加了一些繁瑣工作
四層比三層多了一些繁瑣的檔案建立:對每個資源都要提取介面和實現,依賴注入等,這些工作都很繁瑣,所以我們才寫了一個工具
db2entity
,把這些工作交由一個工具解決。
探索的過程可能很痛苦,但是探索出成果後會有成就感,這估計就是探索的樂趣了。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 微服務2:微服務全景架構微服務架構
- 金融行業微服務架構解析行業微服務架構
- 微服務架構:構建PHP微服務生態微服務架構PHP
- [雲原生微服務架構](十)微服務架構的基礎知識微服務架構
- 微服務架構初探微服務架構
- 微服務 dubbospring 架構微服務Spring架構
- 單體架構&微服務架構&中臺服務架構架構微服務
- 微服務架構(一):什麼是微服務微服務架構
- Spring Cloud雲服務架構 - 企業分散式微服務雲架構構建SpringCloud架構分散式微服務
- 微服務架構—服務降級微服務架構
- 《微服務架構設計模式》讀書筆記 | 第5章 微服務架構中的業務邏輯設計微服務架構設計模式筆記
- 微服務架構和設計模式 - DZone微服務微服務架構設計模式
- SpringCloud(1) ——回顧微服務和微服務架構SpringGCCloud微服務架構
- 架構演進之「微服務架構」架構微服務
- 架構之:微服務架構漫談架構微服務
- SOA架構和微服務架構的區別架構微服務
- 整合spring cloud雲服務架構 - 企業分散式微服務雲架構構建SpringCloud架構分散式微服務
- 微服務核心架構梳理微服務架構
- 微服務架構初識微服務架構
- 微服務架構詳談微服務架構
- 微服務與架構師微服務架構
- 聊聊微服務架構思想微服務架構
- 趣頭條-誠招微服務架構/業務架構/中介軟體架構/演算法微服務架構演算法
- 微服務架構中的分散式事務全面詳解 -DZone微服務微服務架構分散式
- 【分散式微服務企業快速架構】SpringCloud分散式、微服務、雲架構快速開發平臺分散式微服務架構SpringGCCloud
- 能顯示業務目標的DDD微服務架構圖 -Aleix微服務架構
- 微服務架構之「 服務註冊 」微服務架構
- 微服務架構中的服務發現策略微服務架構
- 如何拆分你的微服務架構?微服務架構
- 微服務下的資料架構微服務架構
- 微服務架構在阿里的演化微服務架構阿里
- C#中的微服務架構C#微服務架構
- 微服務開發攻略之淺析微服務架構微服務架構
- (四)整合spring cloud雲服務架構 - 企業分散式微服務雲架構構建SpringCloud架構分散式微服務
- 軟體架構模式之微服務架構架構模式微服務
- 分散式架構和微服務架構的區別分散式架構微服務
- spring微服務架構設計與輕量級微服務架構及最佳部署Spring微服務架構
- 微服務架構學習與思考(05):微服務架構適用場景分析微服務架構