simon

simon

github

Functional Options Pattern 缺省传参的Go函数

Functional Options Pattern: 缺省传参的 Go 函数#

Created: May 4, 2021 2:32 PM
Tags: Golang

1. 问题#

在使用 Golang 的过程中,发现相对于 Python 它缺少一些语法糖,例如函数的参数可以有默认值,有默认值的参数可以不传。Java 虽然没有这种语法糖,但是他有重载可以实现类似的效果

exsample

def exsample(must_have, option_value = "world"):
    print("{} {}".format(must_have + option_value))

exsample("hello")
output: hello world

exsample("hello", "friends")
output: hello friends

今天写 UT,需要构造数据,发现我们的构造数据方式有两种

  1. 直接使用 ORM 创建一个实例,参数值自定义
  2. 写一个 factory,用 faker 造一些数据,部分指定,部分使用 fake 的数据

这两个方法都有比较明显的缺陷

方法 1,每次都要构造所有的字段,很麻烦

方法 2,无法自定义实例的参数,在我们构造相关联的两个实例时,无法指定关联字段

2. 期望#

那么,有没有既可以最大限度的使用 faker 来造数据,同时又可以方便的自定某些参数的值进行关联呢?我们来看下 Python 里的 factory boy 是怎么做的

class ModelAFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = ModelA

    name = factory.Sequence(lambda n: "ModelA%s" % n)
    description = factory.fuzzy.FuzzyText()
    pic = factory.Sequence(lambda n: "image%s" % n)
    status = Status.ACTIVE.value

    @factory.post_generation
    def related_models(obj, create, extracted, **kwargs):
        RelatedModelFactory.create(model_a=obj, **kwargs)

ModelAFactory 在 meta 中定义了 Model,然后自定义了其中 4 个字段的生成方式,其他字段用默认方式生成。

使用时,一种是完全使用 factory 造的参数,另一种是可以指定部分字段的值。例子我举的是 name,更常见的情况是关联其他表的字段。

instance_a = ModelAFactory()  # 完全依赖factory
instance_b = ModelAFactory(name="option")  # 自定了instance name的值

3. 方案及实现#

3.1 背景知识#

Function types 函数类型 [1]#

  • 自定义一个类型 (type),代表一系列类似的函数的集合 (func)
  • 这些函数有相同的输入和输出
// 名字为TypeName的类型,是一系列函数的抽象类型,这些函数的共同特点是,
// 接收一个string参数,返回一个string参数
type TypeName func(var1 string) string

// 给TypeName类型添加一个方法say,所有TypeName的类型的函数都会有这个方法函数
func (typeName TypeName) say(n string) {
    fmt.Println(typeName(n))  // 打印出某个TypeName类型的函数在传入参数n后的返回值
}

// 我们来实现一个TypeName类型的函数方法
func english(name string) string {
    return "Hello, " + name
}

func french(name string) string {
    return "Bonjour, " + name
}

func main() {
    // 将english函数转换为TypeName类型,这样给english添加了一个方法say()
    g := TypeName(english)
    g.say("World")
    g = Greeting(french)
    g.say("World")
}

Closure 闭包#

  • 概念

    a closure is a record storing a function together with an environment.
    闭包是由函数和与其相关的引用环境组合而成的实体 。

    • 函数是指外部函数和内部函数
      • 外部函数主要是为了包裹环境,主要是为了构造一个带有环境的闭包函数
      • 内部函数是实际需要执行函数,也就是闭包函数本身
      • 闭包函数返回一个可执行的函数(内部函数)
    • 环境
      • 所谓环境,其实就是指外部函数传入的参数值,或者内部的参数,会被保存在闭包函数中,并且可以运用到内部函数的计算
    • 同一个闭包函数,多次调用,会各自独立

    这个概念在 Go 里面内容较多,这里只介绍足够我们实现当前功能的内容

  • 闭包函数例子

    func foo(x int) func() {
        return func(y int) {
            fmt.Printf("foo val = %d\n", x + y)
        }
    }
    
    // 初始化一个闭包函数, 1赋值给了foo的参数x,返回一个闭包函数(也就是带有x值的 func(y int))
    f1 := foo(1)
    // 执行闭包函数, y=2, x=1, 执行func中的匿名函数
    f1(2)
    // output:foo val = 3
    

Go 函数可变数量入参#

  • Go 函数有变参数,即可以传入不定数量的同类型参数,go 的 print 函数就是利用这个特性。函数的入参可以是各种类型,包括自定类型,例如 function types
  • 表示方式如下
// ...就表示有未数量的string参数可以传入
func test (v1 string, ...vs string) {
    fmt.Println(v1)
    for _, s := range vs {
       fmt.Println(s)
    }
}

使用起来,如下

test("A")
Out: "A"
test("A", "B", "C")
Out: "A"
     "B"
     "C"

3.2 方案#

Functional Options Pattern 函数选择模式 [2] [3]。

有了前面的知识,我们就可以来了解本文的主角 — 函数选择模式,它通过函数的不同参数的变化,修改或增加函数本身的功能。

3.2.1 例子#

h := NewHouse(
   WithConcrete(),
   WithoutFireplace()
)

NewHouse 在这里是一个构造函数,WithConcrete()WithFireplace()是构造函数的可选参数,这些参数可以修改函数的返回值。

3.2.2 定义构造函数#

// 定义一个结构体,里面存放我们可选参数的值
type User struct {
    Name     string
}

// 构造函数, 默认名为张三
func NewUser() User {
    options := User{}
    options.Name = "张三"
    return options
}

3.2.3 定义函数选项#

外部函数 WithName 传入 Name 字段对应想要自定义的值 name,返回一个 UserOption 类型的函数,也就是 return 的那个匿名函数。函数的作用是将 name 的参数值复制到,传入可选参数结构体的指针的对应字段 Name,这样赋值后,函数外的 * User 值也会修改。

//  定义一个自定义的函数类型,作为闭包函数的返回
//  入参为可变参数struct的指针,因为需要修改闭包外的值作为存储
type UserOption func(options *User)

// 创建一个选项函数
func WithName(name string) UserOption {
	return func(options *User) {
		options.Name = name
	}
}

3.2.4 将函数选项添加到构造函数#

此时我们需要修改一下之前的构造函数,让其支持传入多个函数选项。"...UserOption" 表示可以传入未知数量的 UserOption 类型的函数

func NewUser(opts ...UserOption) User {
	options := User{}
	options.Name = "张三"
  // 循环执行选项函数,进行字段的赋值
	for _, o := range opts {
		o(&options)
	}
	return options
}

使用,注意下,WithName 这个函数其实用闭包生成一个带有可赋值参数值的 UserOption 类型函数,然后构造函数只要将存储值的 struct 传入执行一下,就可以把里面的值替换掉

func main() {
	fmt.Println("Hello, playground")
  // 使用默认值
	user := NewUser()
	fmt.Println("Name", user.Name)
  // 使用自定义值
	user2 := NewUser(WithName("黄子龙"))
	fmt.Println("Name", user2.Name)
}

4. Show me the code!#

// wechat 相关.
// 定义一个类型,可以是函数,传入值是我们的model,因为使用闭包保证赋值,所以没有传出.
type WechatOption func(options *entschema.WeChat)

// 用闭包进行自定义赋值.
func WithAppIDWechat(s string) WechatOption {
	return func(options *entschema.WeChat) {
		options.AppID = s
	}
}

func WithUnionIDWechat(s string) WechatOption {
	return func(options *entschema.WeChat) {
		options.UnionID = &s
	}
}

func WithOpenIDWechat(s string) WechatOption {
	return func(options *entschema.WeChat) {
		options.OpenID = s
	}
}

func WithAccountIDWechat(s uint64) WechatOption {
	return func(options *entschema.WeChat) {
		options.AccountID = s
	}
}

func WithTypeWechat(s wechat.Type) WechatOption {
	return func(options *entschema.WeChat) {
		options.Type = s
	}
}

func WeChatFactory(suite TestSuite, opts ...WechatOption) entschema.WeChat {
	options := entschema.WeChat{}
	// 配置默认的fake数据, fake参数在factory内.
	suite.NoError(faker.FakeData(&options))
	// 传参则使用传入的参数进行赋值,实现可变传参.
	for _, o := range opts {
		o(&options)
	}
	aWechat := app.Database.WeChat.Create().
		SetAppID(options.AppID).
		SetOpenID(options.OpenID).
		SetAccountID(options.AccountID).
		SetUnionID(*options.UnionID).
		SetType(options.Type).
		SaveX(suite.Context())
	return *aWechat
}

wechatFactory(WithUnionIDWechat("id"))

5. One More Thing#

虽然我们学习了这个知识,也确实知道这个东西很不错,但不代表我们会去用。我注意到其实我们不少小伙伴其实有在一些地方用到,但并非所有需要的地方都用到了。

究其原因,还是因为这太麻烦了,要写很多代码,相比于 python 只要定一个参数名,再设置一个默认值的方式。Go 需要实现同样的功能要写多得多的代码,很明显,我们没有这个时间。它也不值得我们花时间这些时间。这算是性能的代价吧。但有没有双全法呢?有的

Generate-function-opts Command#

./generate-function-opts 
   -definitionFile gen/entschema/workwechatuser.go   
   -structTypeName WorkWeChatUser 
   -outFile pkg/testutils/workwechatuserfactory.go  
   -skipStructFields Edges

reference#

  1. Golang.org. 2021. The Go Programming Language Specification - The Go Programming Language. [online] Available at: https://golang.org/ref/spec#Function_types [Accessed 25 February 2021].
  2. talks, C., 2018. Go 函数选项模式. [online] Code talks. Available at: https://lingchao.xin/post/functional-options-pattern-in-go.html [Accessed 25 February 2021].
  3. Sohamkamani.com. 2019. [online] Available at: https://www.sohamkamani.com/golang/options-pattern/ [Accessed 26 February 2021].
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。