前言
在了解表驅(qū)動(dòng)開(kāi)發(fā)之前,有一個(gè)概念需要了解以下,那就是圈復(fù)雜度,又叫循環(huán)復(fù)雜度,那么什么是圈復(fù)雜度呢?
維基百科給出的解釋是:圈復(fù)雜度是用來(lái)度量程序復(fù)雜度的,與時(shí)間復(fù)雜度和空間復(fù)雜度不同的是,圈復(fù)雜度是從程序的控制流程唯獨(dú)來(lái)進(jìn)行度量的,它指程序的控制流程圖中,若將結(jié)束點(diǎn)到起始點(diǎn)再增加一個(gè)邊時(shí),控制流程圖中圈(幾個(gè)邊形成的封閉路徑)的個(gè)數(shù)。
場(chǎng)景引入
幾乎每個(gè)系統(tǒng)中都少不了登錄功能,如果登錄模塊提供多種登錄方式(如微信、Apple、Google、用戶名/密碼、Token等),那么在代碼實(shí)現(xiàn)中你會(huì)怎么實(shí)現(xiàn)呢?相信很多人會(huì)采取如下方式:
type Platform uint8
const (
Wechat Platform = iota + 1
Apple
Account
)
func Login(platform Platform, loginParam interface{}) (err error) {
switch platform {
case Wechat:
return ByWechat(loginParam)
case Apple:
return ByApple(loginParam)
case Account:
return ByAccount(loginParam)
default:
// err
return
}
}
func ByWechat(param interface{}) (err error) {
// logic
return
}
func ByApple(param interface{}) (err error) {
// logic
return
}
func ByAccount(param interface{}) (err error) {
// logic
return
}
或者說(shuō)是定義一個(gè)登錄的方法集(接口),然后不同的方式定義不同的結(jié)構(gòu)體,每個(gè)結(jié)構(gòu)體實(shí)現(xiàn)登錄方法集,最后在統(tǒng)一的登錄入口處同樣通過(guò)switch來(lái)選擇不同的方法集載體。
盡管這樣實(shí)現(xiàn)沒(méi)問(wèn)題,但是值得思考的的一個(gè)點(diǎn)是:如果有更多的登錄方式,那么就需要在switch中添加更多的case,這樣下去的結(jié)果就是代碼難免會(huì)越來(lái)越顯得臃腫,對(duì)于功能復(fù)雜(代碼量大)的模塊來(lái)說(shuō)甚至越往后會(huì)越難維護(hù),那么如何采取一種看起來(lái)美觀,且易于維護(hù)的實(shí)現(xiàn)方式呢?
這里需要插一句,如果代碼中存在很多if,switch的話,會(huì)使代碼的圈復(fù)雜度上升,即讓代碼變得不那么可讀或者維護(hù)性不高。
如何解決?
此時(shí)我們可以通過(guò)表驅(qū)動(dòng)的方式來(lái)優(yōu)化該功能的實(shí)現(xiàn)。什么是表驅(qū)動(dòng)呢?顧名思義,就是通過(guò)(查)表的方法來(lái)改變舊有的邏輯(if...else/switch)語(yǔ)句,尤其是在業(yè)務(wù)中對(duì)于不同途徑的選擇存在大量的邏輯語(yǔ)句時(shí),可以考慮是否可以通過(guò)表驅(qū)動(dòng)的方法來(lái)實(shí)現(xiàn)。
那么對(duì)于上面提到的多種登錄功能,我們可以這樣實(shí)現(xiàn):

- login.go
package login
import "errors"
type Platform uint8
const (
Wechat Platform = iota + 1
Apple
Account
)
type ILogin interface {
BeforeLogin(interface{})
Login(interface{})
AfterLogin(interface{})
}
var (
m = make(map[Platform]ILogin)
)
func Register(platform Platform, method ILogin) {
// 因?yàn)槭窃诿總€(gè)package中的init函數(shù)調(diào)用,所以不需要加鎖
// 如果需要?jiǎng)討B(tài)添加,這里需要考慮并發(fā)
m[platform] = method
}
func Login(platform Platform, param interface{}) (err error) {
iface, ok := m[platform]
if !ok {
err = errors.New("invalid platform")
return
}
iface.BeforeLogin(param)
iface.Login(param)
iface.AfterLogin(param)
return
}
- account.go
package account
import "login"
type accountStruct struct {
// field
}
func init() {
login.Register(login.Account, &accountStruct{})
}
func (a *accountStruct) BeforeLogin(interface{}) {
}
func (a *accountStruct) Login(interface{}) {
}
func (a *accountStruct) AfterLogin(interface{}) {
}
- apple.go
package apple
import "login"
type appleStruct struct {
// field
}
func init() {
login.Register(login.Apple, &appleStruct{})
}
func (a *appleStruct) BeforeLogin(interface{}) {
}
func (a *appleStruct) Login(interface{}) {
}
func (a *appleStruct) AfterLogin(interface{}) {
}
- wechat.go
package wechat
import "login"
type wechatStruct struct {
// field
}
func init() {
login.Register(login.Wechat, &wechatStruct{})
}
func (a *wechatStruct) BeforeLogin(interface{}) {
}
func (a *wechatStruct) Login(interface{}) {
}
func (a *wechatStruct) AfterLogin(interface{}) {
}
這樣看起來(lái)代碼是否更加清晰直觀呢?如果需要添加更多的登錄方式只需要在新的package中實(shí)現(xiàn)對(duì)應(yīng)的API,同時(shí)在init函數(shù)中注冊(cè)對(duì)應(yīng)的登錄方式,即可在入口函數(shù)處調(diào)用!
最后
這種寫(xiě)法在grpc-go中也能找到。