什么是functional option模式
当我们遇到一定要初始化一个类的时候,大部分时候,我们都会使用类似下列的 New 方法:
package newdemo
type Foo struct {
name string
id int
age int
db interface{}
}
func NewFoo(name string, id int, age int, db interface{}) *Foo {
return &Foo{
name: name,
id: id,
age: age,
db: db,
}
}
在这段代码中,我们定义一个 NewFoo 方法,其中存放初始化 Foo 结构所需要的各种字段属性。
如果你觉得这个写法ok,那请你思考一下几个问题:
- 当参数变多,比如超过5个
- 调用者希望更加灵活易用,比如这么多字段能不能给默认值,并能根据自己的需求自行设置部分字段
如何满足呢?这时就该今天的主角出场了-- funtion optional模式。
funtion optional写法,顾名思义,就是将所有可选的参数作为一个可选方式,一般我们会设计一个“函数类型”来代表这个 Option,然后配套将所有可选字段设计为一个这个函数类型的具体实现。在具体的使用的时候,使用可变字段的方式来控制有多少个函数类型会被执行。比如上述的代码,我们会改造为:
type Foo struct {
name string
id int
age int
db interface{}
}
// FooOption 代表可选参数
type FooOption func(foo *Foo)
// WithName 代表Name为可选参数
func WithName(name string) FooOption {
return func(foo *Foo) {
foo.name = name
}
}
// WithAge 代表age为可选参数
func WithAge(age int) FooOption {
return func(foo *Foo) {
foo.age = age
}
}
// WithDB 代表db为可选参数
func WithDB(db interface{}) FooOption {
return func(foo *Foo) {
foo.db = db
}
}
// NewFoo 代表初始化
func NewFoo(id int, options ...FooOption) *Foo {
foo := &Foo{
name: "default",
id: id,
age: 10,
db: nil,
}
for _, option := range options {
option(foo)
}
return foo
}
我们创建了一个 FooOption 的函数类型,这个函数类型代表的函数结构是 func(foo *Foo) 。这个结构很简单,就是将 foo 指针传递进去,能让内部函数进行修改。然后我们针对三个初始化字段 name,age,db 定义了三个返回了 FooOption 的函数,负责修改它们:WithName;WithAge;WithDB。以 WithName 为例,这个函数参数为 string,返回值为 FooOption。在返回值的 FooOption 中,根据参数修改了 Foo 指针。
这样我们前面提出的问题是否就可以引刃而解了,也就是:
- 参数变多,只对需要自定义的字段通过WithXXX设置即可
- 初始化变得简洁而可扩展,可以使用默认值,也可以自定义。
通过改造后,就变成了下面这个样子
// 具体使用NewFoo的函数
func Bar() {
foo := NewFoo(1, WithAge(15), WithName("foo"))
fmt.Println(foo)
}
那么这种optinal模式是如何运用到实际开发中的呢,接下来我们以开源项目devstream为例,来看看如何使用optional模式重构NewHelm的初始化函数
先看下原来的写法
//pkg/util/helm/helm.go
func NewHelm(param *HelmParam) (*Helm, error) {
var hClient helmclient.Client
var err error
if hClient, err = helmclient.New(
&helmclient.Options{
Namespace: param.Chart.Namespace,
RepositoryCache: "/tmp/.helmcache",
RepositoryConfig: "/tmp/.helmrepo",
Debug: true,
},
); err != nil {
return nil, err
}
tmout, err := time.ParseDuration(param.Chart.Timeout)
if err != nil {
return nil, err
}
entry := &repo.Entry{
Name: param.Repo.Name,
URL: param.Repo.URL,
Username: "",
Password: "",
CertFile: "",
KeyFile: "",
CAFile: "",
InsecureSkipTLSverify: false,
PassCredentialsAll: false,
}
// 'Wait' will automatically be set to true when using Atomic.
atomic := true
if !param.Chart.Wait {
atomic = false
}
chartSpec := &helmclient.ChartSpec{
ReleaseName: param.Chart.ReleaseName,
ChartName: param.Chart.ChartName,
Namespace: param.Chart.Namespace,
ValuesYaml: param.Chart.ValuesYaml,
Version: param.Chart.Version,
CreateNamespace: false,
DisableHooks: false,
Replace: true,
Wait: param.Chart.Wait,
DependencyUpdate: false,
Timeout: tmout,
GenerateName: false,
NameTemplate: "",
Atomic: atomic,
SkipCRDs: false,
UpgradeCRDs: param.Chart.UpgradeCRDs,
SubNotes: false,
Force: false,
ResetValues: false,
ReuseValues: false,
Recreate: false,
MaxHistory: 0,
CleanupOnFail: false,
DryRun: false,
}
helm := &Helm{
Entry: entry,
ChartSpec: chartSpec,
Client: hClient,
}
if err = helm.AddOrUpdateChartRepo(*helm.Entry); err != nil {
return nil, err
}
return helm, nil
}
这个构造函数内部需要的参数较多,还有很多内部对象的构建,这样写不仅需要传入很多参数,而且对测试来说如何覆盖一个逻辑复杂这么的构造函数也很头疼的问题。
这个时候我们就可以通过optional模式来重构一下这个构造函数。
先定义一个option函数
type Option func(*Helm)
然后在通过不定长参数的形式,传入到构造函数中
func NewHelm(param *HelmParam, option ...Option) (*Helm, error) {
hClient, err := helmclient.New(
&helmclient.Options{
Namespace: param.Chart.Namespace,
RepositoryCache: "/tmp/.helmcache",
RepositoryConfig: "/tmp/.helmrepo",
Debug: true,
},
)
...
}
再通过WithXXX函数给用户提供自定义的扩展设置接口
func WithEntry(entry *repo.Entry) Option {
return func(r *Helm) {
r.Entry = entry
}
}
func WithChartSpec(spec *helmclient.ChartSpec) Option {
return func(r *Helm) {
r.ChartSpec = spec
}
}
func WithClient(client helmclient.Client) Option {
return func(r *Helm) {
r.Client = client
}
}
最后,调用构造函数
//pkg/util/helm/helm_test.go
func TestNewHelm(t *testing.T) {
got, err := NewHelm(helmParam, WithClient(&DefaultMockClient{}))
if err != nil {
t.Errorf("error: %v\n", err)
}
if got == nil {
t.Errorf("got: %v must not be nil\n", got)
}
got, err = NewHelm(helmParam, WithClient(&DefaultMockClient4{}))
if err != NormalError {
t.Errorf("error: %v must be %v\n", err, NormalError)
}
if got != nil {
t.Errorf("got: %v must be nil\n", got)
}
}
可以看到,调用方可以根据自己的需求控制复杂client对象从外部传入,同时轻松实现个性化设置。
总结:
function optional模式在现实开发中,被大量使用,主要使用场景为:
- 参数较多的构造函数
- 构造函数内部逻辑复杂,需要满足不同调用方对构造对象的不同构造需求
链接:
- https://github.com/devstream-io/devstream
- https://github.com/devstream-io/devstream/commit/2997bb65228c2acfb35965d8b335c6b85605a0f3