原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch5-web/ch5-08-interface-and-web.html
在 Web 項(xiàng)目中經(jīng)常會(huì)遇到外部依賴(lài)環(huán)境的變化,比如:
嗯,所以你看到了,我們的外部依賴(lài)總是為了自己爽而不斷地做升級(jí),且不想做向前兼容,然后來(lái)給我們下最后通牒。如果我們的部門(mén)工作飽和,領(lǐng)導(dǎo)強(qiáng)勢(shì),那么有時(shí)候也可以倒逼依賴(lài)方來(lái)做兼容。但世事不一定如人愿,即使我們的領(lǐng)導(dǎo)強(qiáng)勢(shì),讀者朋友的領(lǐng)導(dǎo)也還是可能認(rèn)慫的。
我們可以思考一下怎么緩解這個(gè)問(wèn)題。
互聯(lián)網(wǎng)公司只要可以活過(guò)三年,工程方面面臨的首要問(wèn)題就是代碼膨脹。系統(tǒng)的代碼膨脹之后,可以將系統(tǒng)中與業(yè)務(wù)本身流程無(wú)關(guān)的部分做拆解和異步化。什么算是業(yè)務(wù)無(wú)關(guān)呢,比如一些統(tǒng)計(jì)、反作弊、營(yíng)銷(xiāo)發(fā)券、價(jià)格計(jì)算、用戶(hù)狀態(tài)更新等等需求。這些需求往往依賴(lài)于主流程的數(shù)據(jù),但又只是掛在主流程上的旁支,自成體系。
這時(shí)候我們就可以把這些旁支拆解出去,作為獨(dú)立的系統(tǒng)來(lái)部署、開(kāi)發(fā)以及維護(hù)。這些旁支流程的時(shí)延如若非常敏感,比如用戶(hù)在界面上點(diǎn)了按鈕,需要立刻返回(價(jià)格計(jì)算、支付),那么需要與主流程系統(tǒng)進(jìn)行 RPC 通信,并且在通信失敗時(shí),要將結(jié)果直接返回給用戶(hù)。如果時(shí)延不敏感,比如抽獎(jiǎng)系統(tǒng),結(jié)果稍后公布的這種,或者非實(shí)時(shí)的統(tǒng)計(jì)類(lèi)系統(tǒng),那么就沒(méi)有必要在主流程里為每一套系統(tǒng)做一套 RPC 流程。我們只要將下游需要的數(shù)據(jù)打包成一條消息,傳入消息隊(duì)列,之后的事情與主流程一概無(wú)關(guān)(當(dāng)然,與用戶(hù)的后續(xù)交互流程還是要做的)。
通過(guò)拆解和異步化雖然解決了一部分問(wèn)題,但并不能解決所有問(wèn)題。隨著業(yè)務(wù)發(fā)展,單一職責(zé)的模塊也會(huì)變得越來(lái)越復(fù)雜,這是必然的趨勢(shì)。一件事情本身變的復(fù)雜的話,這時(shí)候拆解和異步化就不靈了。我們還是要對(duì)事情本身進(jìn)行一定程度的封裝抽象。
最基本的封裝過(guò)程,我們把相似的行為放在一起,然后打包成一個(gè)一個(gè)的函數(shù),讓自己雜亂無(wú)章的代碼變成下面這個(gè)樣子:
func BusinessProcess(ctx context.Context, params Params) (resp, error){
ValidateLogin()
ValidateParams()
AntispamCheck()
GetPrice()
CreateOrder()
UpdateUserStatus()
NotifyDownstreamSystems()
}
不管是多么復(fù)雜的業(yè)務(wù),系統(tǒng)內(nèi)的邏輯都是可以分解為 step1 -> step2 -> step3 ...
這樣的流程的。
每一個(gè)步驟內(nèi)部也會(huì)有復(fù)雜的流程,比如:
func CreateOrder() {
ValidateDistrict() // 判斷是否是地區(qū)限定商品
ValidateVIPProduct() // 檢查是否是只提供給 vip 的商品
GetUserInfo() // 從用戶(hù)系統(tǒng)獲取更詳細(xì)的用戶(hù)信息
GetProductDesc() // 從商品系統(tǒng)中獲取商品在該時(shí)間點(diǎn)的詳細(xì)信息
DecrementStorage() // 扣減庫(kù)存
CreateOrderSnapshot() // 創(chuàng)建訂單快照
return CreateSuccess
}
在閱讀業(yè)務(wù)流程代碼時(shí),我們只要閱讀其函數(shù)名就能知曉在該流程中完成了哪些操作,如果需要修改細(xì)節(jié),那么就繼續(xù)深入到每一個(gè)業(yè)務(wù)步驟去看具體的流程。寫(xiě)得稀爛的業(yè)務(wù)流程代碼則會(huì)將所有過(guò)程都堆積在少數(shù)的幾個(gè)函數(shù)中,從而導(dǎo)致幾百甚至上千行的函數(shù)。這種意大利面條式的代碼閱讀和維護(hù)都會(huì)非常痛苦。在開(kāi)發(fā)的過(guò)程中,一旦有條件應(yīng)該立即進(jìn)行類(lèi)似上面這種方式的簡(jiǎn)單封裝。
業(yè)務(wù)發(fā)展的早期,是不適宜引入接口(interface)的,很多時(shí)候業(yè)務(wù)流程變化很大,過(guò)早引入接口會(huì)使業(yè)務(wù)系統(tǒng)本身增加很多不必要的分層,從而導(dǎo)致每次修改幾乎都要全盤(pán)否定之前的工作。
當(dāng)業(yè)務(wù)發(fā)展到一定階段,主流程穩(wěn)定之后,就可以適當(dāng)?shù)厥褂媒涌趤?lái)進(jìn)行抽象了。這里的穩(wěn)定,是指主流程的大部分業(yè)務(wù)步驟已經(jīng)確定,即使再進(jìn)行修改,也不會(huì)進(jìn)行大規(guī)模的變動(dòng),而只是小修小補(bǔ),或者只是增加或刪除少量業(yè)務(wù)步驟。
如果我們?cè)陂_(kāi)發(fā)過(guò)程中,已經(jīng)對(duì)業(yè)務(wù)步驟進(jìn)行了良好的封裝,這時(shí)候進(jìn)行接口抽象化就會(huì)變的非常容易,偽代碼:
// OrderCreator 創(chuàng)建訂單流程
type OrderCreator interface {
ValidateDistrict() // 判斷是否是地區(qū)限定商品
ValidateVIPProduct() // 檢查是否是只提供給 vip 的商品
GetUserInfo() // 從用戶(hù)系統(tǒng)獲取更詳細(xì)的用戶(hù)信息
GetProductDesc() // 從商品系統(tǒng)中獲取商品在該時(shí)間點(diǎn)的詳細(xì)信息
DecrementStorage() // 扣減庫(kù)存
CreateOrderSnapshot() // 創(chuàng)建訂單快照
}
我們只要把之前寫(xiě)過(guò)的步驟函數(shù)簽名都提到一個(gè)接口中,就可以完成抽象了。
在進(jìn)行抽象之前,我們應(yīng)該想明白的一點(diǎn)是,引入接口對(duì)我們的系統(tǒng)本身是否有意義,這是要按照?qǐng)鼍叭ミM(jìn)行分析的。假如我們的系統(tǒng)只服務(wù)一條產(chǎn)品線,并且內(nèi)部的代碼只是針對(duì)很具體的場(chǎng)景進(jìn)行定制化開(kāi)發(fā),那么引入接口是不會(huì)帶來(lái)任何收益的。至于說(shuō)是否方便測(cè)試,這一點(diǎn)我們會(huì)在之后的章節(jié)來(lái)講。
如果我們正在做的是平臺(tái)系統(tǒng),需要由平臺(tái)來(lái)定義統(tǒng)一的業(yè)務(wù)流程和業(yè)務(wù)規(guī)范,那么基于接口的抽象就是有意義的。舉個(gè)例子:
圖 5-19 實(shí)現(xiàn)公有的接口
平臺(tái)需要服務(wù)多條業(yè)務(wù)線,但數(shù)據(jù)定義需要統(tǒng)一,所以希望都能走平臺(tái)定義的流程。作為平臺(tái)方,我們可以定義一套類(lèi)似上文的接口,然后要求接入方的業(yè)務(wù)必須將這些接口都實(shí)現(xiàn)。如果接口中有其不需要的步驟,那么只要返回 nil
,或者忽略就好。
在業(yè)務(wù)進(jìn)行迭代時(shí),平臺(tái)的代碼是不用修改的,這樣我們便把這些接入業(yè)務(wù)當(dāng)成了平臺(tái)代碼的插件(plugin)引入進(jìn)來(lái)了。如果沒(méi)有接口的話,我們會(huì)怎么做?
import (
"sample.com/travelorder"
"sample.com/marketorder"
)
func CreateOrder() {
switch businessType {
case TravelBusiness:
travelorder.CreateOrder()
case MarketBusiness:
marketorder.CreateOrderForMarket()
default:
return errors.New("not supported business")
}
}
func ValidateUser() {
switch businessType {
case TravelBusiness:
travelorder.ValidateUserVIP()
case MarketBusiness:
marketorder.ValidateUserRegistered()
default:
return errors.New("not supported business")
}
}
// ...
switch ...
switch ...
switch ...
沒(méi)錯(cuò),就是無(wú)窮無(wú)盡的 switch
,和沒(méi)完沒(méi)了的垃圾代碼。引入了接口之后,我們的 switch
只需要在業(yè)務(wù)入口做一次。
type BusinessInstance interface {
ValidateLogin()
ValidateParams()
AntispamCheck()
GetPrice()
CreateOrder()
UpdateUserStatus()
NotifyDownstreamSystems()
}
func entry() {
var bi BusinessInstance
switch businessType {
case TravelBusiness:
bi = travelorder.New()
case MarketBusiness:
bi = marketorder.New()
default:
return errors.New("not supported business")
}
}
func BusinessProcess(bi BusinessInstance) {
bi.ValidateLogin()
bi.ValidateParams()
bi.AntispamCheck()
bi.GetPrice()
bi.CreateOrder()
bi.UpdateUserStatus()
bi.NotifyDownstreamSystems()
}
面向接口編程,不用關(guān)心具體的實(shí)現(xiàn)。如果對(duì)應(yīng)的業(yè)務(wù)在迭代中發(fā)生了修改,所有的邏輯對(duì)平臺(tái)方來(lái)說(shuō)也是完全透明的。
Go 被人稱(chēng)道的最多的地方是其接口設(shè)計(jì)的正交性,模塊之間不需要知曉相互的存在,A 模塊定義接口,B 模塊實(shí)現(xiàn)這個(gè)接口就可以。如果接口中沒(méi)有 A 模塊中定義的數(shù)據(jù)類(lèi)型,那 B 模塊中甚至都不用 import A
。比如標(biāo)準(zhǔn)庫(kù)中的 io.Writer
:
type Writer interface {
Write(p []byte) (n int, err error)
}
我們可以在自己的模塊中實(shí)現(xiàn) io.Writer
接口:
type MyType struct {}
func (m MyType) Write(p []byte) (n int, err error) {
return 0, nil
}
那么我們就可以把我們自己的 MyType
傳給任何使用 io.Writer
作為參數(shù)的函數(shù)來(lái)使用了,比如:
package log
func SetOutput(w io.Writer) {
output = w
}
然后:
package my-business
import "xy.com/log"
func init() {
log.SetOutput(MyType)
}
在 MyType
定義的地方,不需要 import "io"
就可以直接實(shí)現(xiàn) io.Writer
接口,我們還可以隨意地組合很多函數(shù),以實(shí)現(xiàn)各種類(lèi)型的接口,同時(shí)接口實(shí)現(xiàn)方和接口定義方都不用建立 import 產(chǎn)生的依賴(lài)關(guān)系。因此很多人認(rèn)為 Go 的這種正交是一種很優(yōu)秀的設(shè)計(jì)。
但這種 “正交” 性也會(huì)給我們帶來(lái)一些麻煩。當(dāng)我們接手了一個(gè)幾十萬(wàn)行的系統(tǒng)時(shí),如果看到定義了很多接口,例如訂單流程的接口,我們希望能直接找到這些接口都被哪些對(duì)象實(shí)現(xiàn)了。但直到現(xiàn)在,這個(gè)簡(jiǎn)單的需求也就只有 Goland 實(shí)現(xiàn)了,并且體驗(yàn)尚可。Visual Studio Code 則需要對(duì)項(xiàng)目進(jìn)行全局掃描,來(lái)看到底有哪些結(jié)構(gòu)體實(shí)現(xiàn)了該接口的全部函數(shù)。那些顯式實(shí)現(xiàn)接口的語(yǔ)言,對(duì)于 IDE 的接口查找來(lái)說(shuō)就友好多了。另一方面,我們看到一個(gè)結(jié)構(gòu)體,也希望能夠立刻知道這個(gè)結(jié)構(gòu)體實(shí)現(xiàn)了哪些接口,但也有著和前面提到的相同的問(wèn)題。
雖有不便,接口帶給我們的好處也是不言而喻的:一是依賴(lài)反轉(zhuǎn),這是接口在大多數(shù)語(yǔ)言中對(duì)軟件項(xiàng)目所能產(chǎn)生的影響,在 Go 的正交接口的設(shè)計(jì)場(chǎng)景下甚至可以去除依賴(lài);二是由編譯器來(lái)幫助我們?cè)诰幾g期就能檢查到類(lèi)似 “未完全實(shí)現(xiàn)接口” 這樣的錯(cuò)誤,如果業(yè)務(wù)未實(shí)現(xiàn)某個(gè)流程,但又將其實(shí)例作為接口強(qiáng)行來(lái)使用的話:
package main
type OrderCreator interface {
ValidateUser()
CreateOrder()
}
type BookOrderCreator struct{}
func (boc BookOrderCreator) ValidateUser() {}
func createOrder(oc OrderCreator) {
oc.ValidateUser()
oc.CreateOrder()
}
func main() {
createOrder(BookOrderCreator{})
}
會(huì)報(bào)出下述錯(cuò)誤。
# command-line-arguments
./a.go:18:30: cannot use BookOrderCreator literal (type BookOrderCreator) as type OrderCreator in argument to createOrder:
BookOrderCreator does not implement OrderCreator (missing CreateOrder method)
所以接口也可以認(rèn)為是一種編譯期進(jìn)行檢查的保證類(lèi)型安全的手段。
熟悉開(kāi)源 lint 工具的同學(xué)應(yīng)該見(jiàn)到過(guò)圈復(fù)雜度的說(shuō)法,在函數(shù)中如果有 if
和 switch
的話,會(huì)使函數(shù)的圈復(fù)雜度上升,所以有強(qiáng)迫癥的同學(xué)即使在入口一個(gè)函數(shù)中有 switch
,還是想要干掉這個(gè) switch
,有沒(méi)有什么辦法呢?當(dāng)然有,用表驅(qū)動(dòng)的方式來(lái)存儲(chǔ)我們需要實(shí)例:
func entry() {
var bi BusinessInstance
switch businessType {
case TravelBusiness:
bi = travelorder.New()
case MarketBusiness:
bi = marketorder.New()
default:
return errors.New("not supported business")
}
}
可以修改為:
var businessInstanceMap = map[int]BusinessInstance {
TravelBusiness : travelorder.New(),
MarketBusiness : marketorder.New(),
}
func entry() {
bi := businessInstanceMap[businessType]
}
表驅(qū)動(dòng)的設(shè)計(jì)方式,很多設(shè)計(jì)模式相關(guān)的書(shū)籍并沒(méi)有把它作為一種設(shè)計(jì)模式來(lái)講,但我認(rèn)為這依然是一種非常重要的幫助我們來(lái)簡(jiǎn)化代碼的手段。在日常的開(kāi)發(fā)工作中可以多多思考,哪些不必要的 switch case
可以用一個(gè)字典和一行代碼就可以輕松搞定。
當(dāng)然,表驅(qū)動(dòng)也不是沒(méi)有缺點(diǎn),因?yàn)樾枰獙?duì)輸入 ?key
?計(jì)算哈希,在性能敏感的場(chǎng)合,需要多加斟酌。
![]() | ![]() |
更多建議: