Go设计模式之函数选项模式

目录

  • 引入
  • 函数选项模式(functional options pattern)
    • 可选参数
    • 默认值
    • 接口类型版本

引入

假设现在需要定义一个包含多个配置项的结构体,具体定义如下:

// DoSomethingOption 定义配置项
type DoSomethingOption struct {
	// a 配置a
	a string
	// b 配置b
	b string
	// c 配置c
	c string
	// ...
}

这个配置结构体中的字段可能有几个也可能有十几个,

现在写一个构造函数(初始化函数):

func NewDoSomethingOption(a,b,c string) *DoSomethingOption {
	return &DoSomethingOption{
		a: a,
		b: b,
		c: c,
	}
}

产生两个问题:

  1. 如果 DoSomethingOption 有十几个字段,构造函数需要定义十几个参数吗?如何为某些配置项指定默认值?
  2. DoSomethingOption 随着业务发展不断新增字段后,构造函数是否也需要同步变更?变更了构造函数是否又会影响已有代码?

函数选项模式(functional options pattern)

函数选项模式(Functional Options Pattern)也称为选项模式(Options Pattern),是一种创造性的设计模式,允许使用接受零个或多个函数作为参数的可变构造函数构建复杂结构。

可选参数

Go语言中的函数不支持默认参数,但又原生支持可变长参数。

可变长参数的具体类型则需要好好设计一下。它必须满足以下条件:

  1. 不同的函数参数拥有相同的类型
  2. 指定函数参数能为特定的配置项赋值
  3. 支持扩展新的配置项

先定义一个名为OptionFunc的类型,它实际上一个接收 *DoSomethingOption 作为参数并且会在函数内部修改其字段的函数。

type OptionFunc func(*DoSomethingOption)

接下来,我们为DoSomethingOption字段编写一系列WithXxx函数,其返回值是一个修改指定字段的闭包函数。

// WithB 将 DoSomethingOption 的 b 字段设置为指定值
func WithB(b string) OptionFunc {
	return func(o *DoSomethingOption) {
		o.b = b
	}
}

// WithC 将 DoSomethingOption 的 b 字段设置为指定值
func WithC(c string) OptionFunc {
	return func(o *DoSomethingOption) {
		o.c = c
	}
}

WithXxx是函数选项模式中约定成俗的函数名称格式,这样构造函数就可以改写成如下方式了,除了必须传递a参数外,其他的参数都是可选的。

func NewDoSomethingOption(a string, opts ...OptionFunc) *DoSomethingOption {
	o := &DoSomethingOption{a: a}
	for _, opt := range opts {
		opt(o)
	}
	return o
}

只想传入ab参数时,可以按如下方式:

NewDoSomethingOption("德玛西亚", WithB(10))

默认值

在使用函数选项模式后,可以很方便的为某些字段设置默认值,例如下面的示例代码中B默认值为100。

const defaultValueB = 100

func NewDoSomethingOption(a string, opts ...OptionFunc) *DoSomethingOption {
	o := &DoSomethingOption{a: a, b: defaultValueB}  // 字段b使用默认值
	for _, opt := range opts {
		opt(o)
	}
	return o
}

以后要为DoSomethingOption添加新的字段时也不会影响之前的代码,只需要为新字段编写对应的With函数即可。

接口类型版本

在一些场景下,并不想对外暴露具体的配置结构体,而是仅仅对外提供一个功能函数,这时可以将对应的结构体定义为小写字母开头,将其限制只在包内部使用。

// doSomethingOption 定义一个内部使用的配置项结构体
type doSomethingOption struct {
	a string
	b int
	c bool
	// ...
}

此时,同样是使用函数选项模式,但可以通过使用接口类型来“隐藏”内部的逻辑。

// IOption 定义一个接口类型
type IOption interface {
	apply(*doSomethingOption)
}

// funcOption 定义funcOption类型,实现 IOption 接口
type funcOption struct {
	f func(*doSomethingOption)
}

func (fo funcOption) apply(o *doSomethingOption) {
	fo.f(o)
}

func newFuncOption(f func(*doSomethingOption)) IOption {
	return &funcOption{
		f: f,
	}
}

// WithB 将b字段设置为指定值的函数
func WithB(b int) IOption {
	return newFuncOption(func(o *doSomethingOption) {
		o.b = b
	})
}

// DoSomething 包对外提供的函数
func DoSomething(a string, opts ...IOption) {
	o := &doSomethingOption{a: a}
	for _, opt := range opts {
		opt.apply(o)
	}
	// 在包内部基于o实现逻辑...
	fmt.Printf("o:%#v\n", o)
}

如此一来,我们只需对外提供一个DoSomething的功能函数和一系列WithXxx函数。对于调用方来说,使用起来也很方便。

DoSomething("q1mi")
DoSomething("q1mi", WithB(100))

你可能感兴趣的:(GO,golang,设计模式,java)