前言
在了解表驱动开发之前,有一个概念需要了解以下,那就是圈复杂度,又叫循环复杂度,那么什么是圈复杂度呢?
维基百科给出的解释是:圈复杂度是用来度量程序复杂度的,与时间复杂度和空间复杂度不同的是,圈复杂度是从程序的控制流程唯独来进行度量的,它指程序的控制流程图中,若将结束点到起始点再增加一个边时,控制流程图中圈(几个边形成的封闭路径)的个数。
场景引入
几乎每个系统中都少不了登录功能,如果登录模块提供多种登录方式(如微信、Apple、Google、用户名/密码、Token等),那么在代码实现中你会怎么实现呢?相信很多人会采取如下方式:
1 | type Platform uint8 |
或者说是定义一个登录的方法集(接口),然后不同的方式定义不同的结构体,每个结构体实现登录方法集,最后在统一的登录入口处同样通过switch
来选择不同的方法集载体。
尽管这样实现没问题,但是值得思考的的一个点是:如果有更多的登录方式,那么就需要在switch
中添加更多的case
,这样下去的结果就是代码难免会越来越显得臃肿,对于功能复杂(代码量大)的模块来说甚至越往后会越难维护,那么如何采取一种看起来美观,且易于维护的实现方式呢?
这里需要插一句,如果代码中存在很多if
,switch
的话,会使代码的圈复杂度上升,即让代码变得不那么可读或者维护性不高。
如何解决?
此时我们可以通过表驱动的方式来优化该功能的实现。什么是表驱动呢?顾名思义,就是通过(查)表的方法来改变旧有的逻辑(if...else
/switch
)语句,尤其是在业务中对于不同途径的选择存在大量的逻辑语句时,可以考虑是否可以通过表驱动的方法来实现。
那么对于上面提到的多种登录功能,我们可以这样实现:
代码实现:
- login.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40package 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) {
// 因为是在每个package中的init函数调用,所以不需要加锁
// 如果需要动态添加,这里需要考虑并发
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package 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{}) {
}
这样看起来代码是否更加清晰直观呢?如果需要添加更多的登录方式只需要在新的package中实现对应的API,同时在init
函数中注册对应的登录方式,即可在入口函数处调用!
最后
这种写法在grpc-go中也能找到。