我查阅了另一种称呼:Functional Options,用来替代函数的展开传参。举例来说,一个函数如果声明超过 4 个参数,就会显得函数声明特别长。当然,也可以声明一个结构体参数,把函数参数当做结构体的字段,这样看起来会精简很多。而 Functional Options 其实也是这种方式的扩展而已。
之前阅读了文章 Functional Options in Go: Implementing the Options Pattern in Golang 之后,感觉内容很实在,推荐大家阅读原文。有时候感觉外语的文章挺实在的,讲的都比较详细。希望不是自己的外语水平有限,看的都是一些新手文章…
假设我们需要提供一个获取 “笔记信息” 的接口,它包括内容、阅读数、作者、创建时间、标签、分类等等信息。但并不是所有的调用方都需要全量的笔记信息,有的调用方可能只需要笔记的作者信息就可以。一般来说,我们会如何处理这种情况呢?如果我们要新返回一个元数据信息,比如是否私密(Private),又该如何处理呢?
函数参数方式
GetV1 的扩展性很差,如果这个接口在项目中使用的地方特别多,要新加一个 withPrivate 的参数,之前所有调用 GetV1 的方法都需要加这个参数。GetV2 是 GetV1 的另一种写法,并没有带来任何优势,甚至还引入了一个劣势:不能一眼看出方法提供了哪些选项。
函数参数声明扩展个数是一个问题,其实,参数赋值也可能是一个问题。如果调用链特别深的话,要将实参从调用链的头部一直传递到尾部,可能会横跨多个别的函数调用。当然,这不是我们讲解的重点。
type NoteClient struct {
}
// func1 通过明确的参数来控制加载的数据
func (cli *NoteClient) GetV1(ctx context.Context,
withContent, withView, vithAuthor, withCreateTime bool) (*NoteInfo, error) {
}
// func2 通过扩展的参数来控制
func (cli *NoteClient) GetV2(ctx context.Context, withOptions ...bool) (*NoteInfo, error) {
}
扩展结构体字段
因为函数的参数声明都是独立展开的,所以参数扩展起来只会导致声明越来越长。但我们可以认为的将参数聚合起来,目前可以参数两种聚合的形式:一种是将参数声明为接受结构体的字段,这样每次实例化结构的时候,先将参数做好初始化;另一种是将参数独立为一个入参结构体对象。
虽然都是参数结构体封装的形式,但这两个结构体的含义确实不相同。从类的角度来说,第一种是扩展了类的成员属性,第二种只是声明了一个结构体入参。现在,如果要扩展是否私密,只需要在结构体中追加新的字段 WithPrivate。
type NoteClient struct {
WithContent bool
WithView bool
WithAuthor bool
}
// 或者单独声明一个参数结构体
type NoteOption struct {
WithContent bool
WithView bool
WithAuthor bool
}
// func1 在结构体中加扩展字段
func (cli *NoteClient) GetV1(ctx context.Context) (*NoteInfo, error) {
}
// func2 通过一个结构体 withOption 参数来控制
func (cli *NoteClinet) GetV2(ctx context.Context, withOption *NoteOption) (*NoteInfo, error) {
}
定义函数选项
NoteOptionFunc
可以看出,这其实是使用了闭包的特性。以 WithContent 函数为例,传递了形参 isCnt,在返回函数内部给 opt 做赋值。这个过程,isCnt 可以理解为是通过引用传递的,外层参数的修改也会影响到内存的元素。不过,这个例子没有体现出这个特性来。
type NoteClient struct {
WithContent bool
WithView bool
WithAuthor bool
}
type NoteOptionFunc func(*NoteClient)
func WithContent(isCnt bool) NoteOptionFunc {
return func(opt *NoteClient) {
opt.WithContent = isCnt
}
}
func WithNoContent() NoteOptionFunc {
return func(opt *NoteClient) {
opt.WithContent = false
}
}
func NewNoteClient(opts ...NoteOptionFunc) *NoteClient {
cli := &NoteClient{}
for _, f := range opts {
f(cli)
}
return cli
}
这种模式在很多地方被称为:最佳实践。不过,我个人觉得有利有弊,也不能所有的参数赋值都使用 Functional Options 的模式,这种模式的缺点就是代码量多,优点是赋值过程非常明确,扩展性强,比较适用于底层中间件之类参数赋值操作。