# 附录3.函数选项模式

## PART1. 概念示例

### 1.1 问题的产生

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

```go
package pattern

type SomeOption struct {
	A string
	B int
	C bool
}
```

这个配置结构体中的字段可能有几个也可能有十几个,并且可能随着业务的发展不断增加新的字段.现在我们需要为其编写一个构造函数,根据经验我们可能会写出类似下面那样的构造函数:

```go
package pattern

type SomeOption struct {
	A string
	B int
	C bool
}

// NewSomeOption SomeOption 的构造函数
func NewSomeOption(a string, b int, c bool) *SomeOption {
	return &SomeOption{
		A: a,
		B: b,
		C: c,
	}
}
```

这样的设计面临着2个问题:

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

### 1.2 函数选项模式

函数选项模式(Functional Options Pattern)也称为选项模式(Options Pattern),是一种**创建型设计模式**,选项模式允许你使用**接受0个或多个函数作为可变参数的构造函数**,以便构建复杂结构.我们将这些函数称为选项,由此得名函数选项模式

#### 1.2.1 可选参数

由于Go语言中的函数不支持默认参数,所以我们想到可以使用**可变长参数**来实现.而这个可变长参数的具体类型则需要好好设计一下.它必须满足以下条件:

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

```go
package pattern

// OptionFunc 函数选项 该类型用于设置 SomeOption 的各属性值(即各选项)
type OptionFunc func(*SomeOption)
```

`OptionFunc`类型本质上是一个函数.该函数接收一个`*SomeOption`作为参数,并在其内部修改这个实例的字段值:

```go
package pattern

// OptionFunc 函数选项 该类型用于设置 SomeOption 的各属性值(即各选项)
type OptionFunc func(*SomeOption)

// WithB 本函数返回一个 OptionFunc ,该 OptionFunc 将
// SomeOption 实例的 B 字段值设置为给定值
func WithB(b int) OptionFunc {
	return func(someOption *SomeOption) {
		someOption.B = b
	}
}

// WithC 本函数返回一个 OptionFunc ,该 OptionFunc 将
// SomeOption 实例的 C 字段值设置为给定值
func WithC(c bool) OptionFunc {
	return func(someOption *SomeOption) {
		someOption.C = c
	}
}
```

那么这样一来,我们的构造函数就可以修改为:

```go
package pattern

type SomeOption struct {
	A string
	B int
	C bool
}

// NewSomeOption SomeOption 的构造函数
func NewSomeOption(a string, options ...OptionFunc) *SomeOption {
	someOption := &SomeOption{
		A: a,
	}

	for _, option := range options {
		option(someOption)
	}
	
	return someOption
}
```

最终创建`SomeOption`实例时的客户端代码就会简单很多:

```go
package main

import (
	"fmt"
	"option/pattern"
)

func main() {
	// 初始化SomeOption时,若SomeOption的字段有变化,仅需调整options切片即可
	options := []pattern.OptionFunc{
		pattern.WithB(10),
		pattern.WithC(true),
	}

	someOption := pattern.NewSomeOption("a", options...)
	fmt.Printf("%#v\n", someOption)
}
```

假如此时需求变更,不再需要初始化`SomeOption`实例时为`SomeOption.B`字段赋值,那么变更就会容易很多:

```go
package main

import (
	"fmt"
	"option/pattern"
)

func main() {
	options := []pattern.OptionFunc{
		pattern.WithC(true),
	}

	someOption := pattern.NewSomeOption("a", options...)
	fmt.Printf("%#v\n", someOption)
}
```

#### 1.2.2 默认值问题

通常我们在初始化实例这个场景下,对默认值的理解是:客户端提供了值,则使用客户端提供的;客户端没提供,则使用默认值.

在函数选项模式中,这种功能就非常容易提供了:

```go
package pattern

type SomeOption struct {
	A string
	B int
	C bool
}

const (
	// defaultValueB SomeOption 实例的 B 字段的默认值
	defaultValueB = 100
)

// NewSomeOption SomeOption 的构造函数
func NewSomeOption(a string, options ...OptionFunc) *SomeOption {
	someOption := &SomeOption{
		A: a,
		B: defaultValueB,
	}

	// 若客户端提供了修改B字段的选项函数 则在此处会覆盖掉默认值
	for _, option := range options {
		option(someOption)
	}

	return someOption
}
```

这样一来,就可以实现提供默认值的需求了.

### 1.3 接口类型的函数选项模式

在一些场景下,我们可能并不想对外暴露具体的配置结构体(也就是我们上述示例中的`SomeOption`),而是仅仅对外提供一个功能函数.这时我们会将对应的结构体定义为小写字母开头,将其限制只在包内部使用:

```go
package pattern

type someOption struct {
	a string
	b int
	c bool
}
```

#### 1.3.1 定义接口

```go
package pattern

// IOption 本接口用于定义函数选项行为
type IOption interface {
	// apply 本方法用于修改 someOption 结构体的字段值
	apply(someOption *someOption)
}
```

#### 1.3.2 定义实现

```go
package pattern

// funcOption 函数选项类型 因为要实现接口,所以不能再使用函数类型,只能使用结构体
type funcOption struct {
	// f 具体选项函数
	withFunc func(someOption *someOption)
}

func (o *funcOption) apply(someOption *someOption) {
	o.withFunc(someOption)
}
```

#### 1.3.3 定义函数选项类型的构造函数

```go
package pattern

// funcOption 函数选项类型 因为要实现接口,所以不能再使用函数类型,只能使用结构体
type funcOption struct {
	// f 具体选项函数
	withFunc func(someOption *someOption)
}

func (o *funcOption) apply(someOption *someOption) {
	o.withFunc(someOption)
}

// newFuncOption funcOption 的构造函数
func newFuncOption(withFunc func(someOption *someOption)) *funcOption {
	return &funcOption{withFunc: withFunc}
}
```

#### 1.3.4 定义各种`With()`函数

注意各种`With()`函数是要对外暴露的,因为客户端要通过这些`With()`函数来修改`someOption`实例的各属性值

```go
package pattern

// WithB 本函数用于修改 someOption 实例的 b 字段值
func WithB(b int) IOption {
	withFunc := func(someOption *someOption) {
		someOption.b = b
	}

	return &funcOption{withFunc: withFunc}
}

// WithC 本函数用于修改 someOption 实例的 c 字段值
func WithC(c bool) IOption {
	withFunc := func(someOption *someOption) {
		someOption.c = c
	}

	return &funcOption{withFunc: withFunc}
}
```

#### 1.3.5 实现对外提供的功能函数

这个功能函数的职责其实和1.2小节中客户端代码的职责是相同的,都是**实例化`someOption`**

```go
package pattern

import "fmt"

// NewSomeOption 本函数是 someOption 的构造函数
func NewSomeOption(a string, options ...IOption) {
	someOptionObj := &someOption{
		a: a,
	}

	for _, option := range options {
		option.apply(someOptionObj)
	}

	fmt.Printf("%#v\n", someOptionObj)
}
```

#### 1.3.6 客户端代码

```go
package main

import "optionWithInterface/pattern"

func main() {
	options := []pattern.IOption{
		pattern.WithB(10),
		pattern.WithC(true),
	}

	pattern.NewSomeOption("a", options...)
}
```

可以看到,使用函数选项模式时,若要实例化的对象上,字段发生了变化,仅需调整`options`即可.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://go.sai.show/part09.appendix/03-fu-lu-.-han-shu-xuan-xiang-mo-shi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
