为你自己学Go
  • README
  • PART01.Web框架概览
    • 1.01 Web框架概览:学习路线
    • 1.02 Web框架概览-Beego框架分析
    • 1.03 Web框架概览-GIN框架分析
    • 1.04 Web框架概览-Iris框架分析
    • 1.05 Web框架概览-Echo框架分析
  • PART02.Server
    • 2.01 Server详解与面试要点
  • PART03.路由树
    • 3.01 路由树-Beego&GIN&Echo实现与设计总结
    • 3.02 路由树-全静态匹配
    • 3.03 路由树-TDD起步
    • 3.04 路由树-静态匹配测试用例
    • 3.05 路由树-静态匹配之路由查找
    • 3.06 路由树-静态匹配之集成Server
    • 3.07 路由树-通配符匹配之路由注册
    • 3.08 路由树-通配符匹配之路由查找与测试
    • 3.09 路由树-参数路径之基本注册和查找
    • 3.10 路由树-参数路径之校验
    • 3.11 路由树-参数路径之参数值
    • 3.12 路由树-总结与面试要点
  • PART04.课后复习
    • 4.01 课后复习-Server
    • 4.02 课后复习-Route
  • PART05.Context
    • 5.01 Context-简介
    • 5.02 Context-Beego Context设计分析
    • 5.03 Context-Gin Context设计分析
    • 5.04 Context-Echo和Iris的Context设计分析
    • 5.05 Context-处理输入输出总结
    • 5.06 Context-处理输入之Body输入
    • 5.07 Context-处理输入之表单输入
    • 5.08 Context-处理输入之查询参数、路径参数和StringValue
    • 5.09 Context-处理输出
    • 5.10 Context-总结与面试要点
  • PART06.AOP
    • 6.01 AOP简介与不同框架设计概览
    • 6.02 AOP设计方案-Middleware
  • PART07.Middleware
    • 7.01 Middleware-AccessLog
    • 7.02 Middleware-Trace简介和OpenTelemetry
    • 7.03 Middleware-OpenTelemetry测试
    • 7.04 Middleware-OpenTelemetry总结
    • 7.05 Prometheus详解
    • 7.06 Middleware-Prometheus
    • 7.07 Middleware-错误页面
    • 7.08 Middleware-从panic中恢复
    • 7.09 Middleware总结和面试
  • PART08.Review
    • 8.01 课后复习-AOP
    • 8.02 课后复习-Context
    • 8.03 课后复习-Middleware-AccessLog
  • PART09.Appendix
    • 附录1.责任链模式
    • 附录2.生成器模式
    • 附录3.函数选项模式
  • xiaochengxu
    • 01.原力去水印
    • 02.KeePass密码管理:安全轻松的管理您的密码
Powered by GitBook
On this page
  • PART1. 概念示例
  • 1.1 问题的产生
  • 1.2 函数选项模式
  • 1.3 接口类型的函数选项模式
  1. PART09.Appendix

附录3.函数选项模式

PART1. 概念示例

1.1 问题的产生

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

package pattern

type SomeOption struct {
	A string
	B int
	C bool
}

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

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

  • 不同的函数参数拥有相同的类型

  • 指定函数参数能为特定的配置项赋值

  • 支持扩展新的配置项

package pattern

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

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

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
	}
}

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

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实例时的客户端代码就会简单很多:

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字段赋值,那么变更就会容易很多:

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 默认值问题

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

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

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),而是仅仅对外提供一个功能函数.这时我们会将对应的结构体定义为小写字母开头,将其限制只在包内部使用:

package pattern

type someOption struct {
	a string
	b int
	c bool
}

1.3.1 定义接口

package pattern

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

1.3.2 定义实现

package pattern

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

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

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

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实例的各属性值

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

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 客户端代码

package main

import "optionWithInterface/pattern"

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

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

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

Last updated 1 year ago