驱动开发在Golang中的应用

前言

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

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

场景引入

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

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

  1. 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{}) {

}
  1. 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{}) {

}
  1. 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{}) {

}

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

最后

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

资料参考

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

你可能感兴趣的:(驱动开发在Golang中的应用)