当我们遇到一定要初始化一个类的时候,大部分时候,我们都会使用类似下列的 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 结构所需要的各种字段属性。
这个写法乍看之下是没啥问题的,但是一旦 Foo 结构内部的字段发生了变化,增加或者减少了,那么这个初始化函数 NewFoo 就怎么看怎么别扭了。
参数继续增加?那么所有调用了这个 NewFoo 方法的地方也都需要进行修改,且按照代码整洁的逻辑,参数多于 5 个,这个函数就很难使用了。而且,如果这 5 个参数都是可有可无的参数,就是有的参数可以不填写,有默认值,比如 age 这个字段,即使我们不填写,在后续的业务逻辑中可能也没有很多影响,那么我在实际调用 NewFoo 的时候,age 这个字段还需要传递 0 值:
foo := NewFoo("wohu", 1, 0, nil)
这里其实有一种更好的写法:使用 Option 写法来进行改造。
Option 写法,顾名思义,就是将所有可选的参数作为一个可选方式,一般我们会设计一个“函数类型”来代表这个 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 指针。
// WithName 代表Name为可选参数
func WithName(name string) FooOption {
return func(foo *Foo) {
foo.name = name
}
}
顺便说一下,这种函数我们一般都以 With 开头,表示我这次初始化带着这个字段。而最后 NewFoo 函数的参数,我们就改造为两个部分:
Option 字段,就是必填字段,假设我们的 Foo 结构实际上只有一个必填字段 id,而其他字段皆是选填的;options 替换;NewFoo(id int, options ...FooOption)
在具体的 NewFoo 实现中,也变化成 2 个步骤:
按照默认值初始化一个 foo 对象;遍历 options 改造这个 foo 对象。
// 具体使用NewFoo的函数
func Bar() {
foo := NewFoo(1, WithAge(15), WithName("foo"))
fmt.Println(foo)
}
如果我们后续 Foo 多了一个可变属性,那么我们只需要多一个 WithXXX 的方法就可以了,而 NewFoo 函数不需要任何变化,调用方只要在指定这个可变属性的地方增加 WithXXX 就可以了,扩展性非常好。