0%

驱动开发在Golang中的应用

前言

在了解表驱动开发之前,有一个概念需要了解以下,那就是圈复杂度,又叫循环复杂度,那么什么是圈复杂度呢?

维基百科给出的解释是:圈复杂度是用来度量程序复杂度的,与时间复杂度空间复杂度不同的是,圈复杂度是从程序的控制流程唯独来进行度量的,它指程序的控制流程图中,若将结束点到起始点再增加一个边时,控制流程图中圈(几个边形成的封闭路径)的个数。

场景引入

几乎每个系统中都少不了登录功能,如果登录模块提供多种登录方式(如微信、Apple、Google、用户名/密码、Token等),那么在代码实现中你会怎么实现呢?相信很多人会采取如下方式:

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
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
}

或者说是定义一个登录的方法集(接口),然后不同的方式定义不同的结构体,每个结构体实现登录方法集,最后在统一的登录入口处同样通过switch来选择不同的方法集载体。

尽管这样实现没问题,但是值得思考的的一个点是:如果有更多的登录方式,那么就需要在switch中添加更多的case,这样下去的结果就是代码难免会越来越显得臃肿,对于功能复杂(代码量大)的模块来说甚至越往后会越难维护,那么如何采取一种看起来美观,且易于维护的实现方式呢?

这里需要插一句,如果代码中存在很多ifswitch的话,会使代码的圈复杂度上升,即让代码变得不那么可读或者维护性不高。

如何解决?

此时我们可以通过表驱动的方式来优化该功能的实现。什么是表驱动呢?顾名思义,就是通过(查)表的方法来改变旧有的逻辑(if...else/switch)语句,尤其是在业务中对于不同途径的选择存在大量的逻辑语句时,可以考虑是否可以通过表驱动的方法来实现。

那么对于上面提到的多种登录功能,我们可以这样实现:

代码目录

代码实现:

  1. 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
    40
    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) {
    // 因为是在每个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
    }

  2. account.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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{}) {

    }
  3. apple.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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{}) {

    }
  4. wechat.go
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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{}) {

    }

这样看起来代码是否更加清晰直观呢?如果需要添加更多的登录方式只需要在新的package中实现对应的API,同时在init函数中注册对应的登录方式,即可在入口函数处调用!

最后

这种写法在grpc-go中也能找到。

资料参考

《Wiki百科》
《Go语言高级编程》