[设计模式 in Golang]选项模式

前言

在很多优秀的Go语言项目中我们都可以见到选项模式(Options Pattern),例如grpc/grpc-go的NewServer函数、uber-go/zap 包的New函数都用到了选项模式。

使用选项模式,我们可以非常华丽地实现——带许多默认值且可选地修改其中某些值的工厂方法或者函数。这弥补了Go没有给参数设置默认值的语法的问题。大大简化开发者创建一个对象的成本,尤其是在对象拥有众多属性时。

如何带默认参数地创建对象

多个工厂方法

在创建一个对象的时候,如果想要有一些默认值,最朴素地,我们可能会为一个对象准备多个工厂方法:

package client

import "time"

const (
	DefaultTimeout = 1 * time.Second
)

type DemoClient struct {
	host     string
	timeout  time.Duration
}

func NewDemoClient(host string) *DemoClient{
	return &DemoClient{
		host:    host,
		timeout: DefaultTimeout,
	}
}

func NewDemoClientWithTimeout(host string, timeout time.Duration) *DemoClient{
	return &DemoClient{
		host:    host,
		timeout: timeout,
	}
}

这种方式十分直观,但是扩展性实在太差,如果多几个参数,比如加个maxretry参数

package client

import "time"

const (
	DefaultTimeout = 1 * time.Second
	DefaultMaxRetry = 3
)

type DemoClient struct {
	host    string
	timeout time.Duration
	maxRetry int
}

那难道我再加:

NewDemoClientWithMaxRetry()
NewDemoClientWithTimeoutAndMaxRetry()

多几个参数还不写疯了。要写排列组合个工厂方法。omg。太不geek了。

提供默认Option参数的工厂

优雅很多地,我们可以让工厂方法接受一个Option结构体,并提供默认的Option。

package client

import "time"

const (
	DefaultTimeout  = 1 * time.Second
	DefaultMaxRetry = 3
)

type DemoClient struct {
	host     string
	timeout  time.Duration
	maxRetry int
}

type Options struct {
	Timeout  time.Duration
	MaxRetry int
}

func NewDefaultOptions() Options {
	return Options{
		Timeout:  DefaultTimeout,
		MaxRetry: DefaultMaxRetry,
	}
}

func NewDemoClient(host string, opt Options) *DemoClient {
	return &DemoClient{
		host:     host,
		timeout:  opt.Timeout,
		maxRetry: opt.MaxRetry,
	}
}

这样构造一个Client的代码就长成这样了:

	opt := NewDefaultOptions()
	opt.Timeout = 3 * time.Second
	client := NewDemoClient("127.0.0.1:8888", opt)

或者也可以不提供DefaultOption,0值如果无意义直接赋值为默认值,如果0值有意义,Option中就改成指针:

package client

import "time"

const (
	DefaultTimeout  = 1 * time.Second
	DefaultMaxRetry = 3
)

type DemoClient struct {
	host     string
	timeout  time.Duration
	maxRetry int
}

type Options struct {
	Timeout  time.Duration
	MaxRetry *int
}

func NewDemoClient(host string, opt Options) *DemoClient {
	client := &DemoClient{
		host:     host,
		timeout:  DefaultTimeout,
		maxRetry: DefaultMaxRetry,
	}
	if opt.Timeout != 0 {
		client.timeout = opt.Timeout
	}
	if opt.MaxRetry != nil {
		client.maxRetry = *opt.MaxRetry
	}
	return client
}

这里把Options.MaxRetry从int改成了*int是应为0值是有意义的,代表不重试。为了能让默认是有重试的,得要让用户显式地指定重试次数,赋值一个指针。

这样,很简单的就能创建一个默认的client。

	client := NewDemoClient("127.0.0.1:8888", Options{})

这种方法其实已经蛮简单,扩展性很好,满足大部分需求了。如果需要加其他有默认值的参数,只需要往Options里加就行,还能兼容以前的代码。唯一一点小缺点就是还是要创建Options,还是麻烦了一点。

如果想要更加geek一点,那我们可以整上选项模式

选项模式

以下代码通过选项模式实现上述功能:

package client

import (
	"time"
)

const (
	DefaultTimeout  = 1 * time.Second
	DefaultMaxRetry = 3
)

type DemoClient struct {
	host     string
	timeout  time.Duration
	maxRetry int
}

type Option func(opt *Options)

type Options struct {
	Timeout  time.Duration
	MaxRetry int
}

var defaultOption = Options{
	Timeout:  DefaultTimeout,
	MaxRetry: DefaultMaxRetry,
}

func WithTimeout(timeout time.Duration) Option {
	return func(opt *Options) {
		opt.Timeout = timeout
	}
}

func WithMaxRetry(maxRetry int) Option {
	return func(opt *Options) {
		opt.MaxRetry = maxRetry
	}
}

func NewDemoClient(host string, opts ...Option) *DemoClient {
	opt := defaultOption
	for _, o := range opts {
		o(&opt)
	}
	return &DemoClient{
		host:     host,
		timeout:  opt.Timeout,
		maxRetry: opt.MaxRetry,
	}
}

在上面的代码中,我们定义了Option函数类型,然后利用Go语言的闭包特性,提供工厂方法来返回按需修改Options各字段的Option函数。在 NewDemoClient中,先用defaultOption初始化默认的Options,然后通过回调

	for _, o := range opts {
		o(&opt)
	}

修改Options内的字段。最后创建出对象。

这里通过利用可变参数列表的特性,同样是构建默认对象,只需要:

client := NewDemoClient("127.0.0.1:8888")

比之前的做法少了个Option{}。

带参数时:

client := NewDemoClient("127.0.0.1:8888", WithMaxRetry(3), WithTimeout(3 * time.Second))

并且这种做法使我们实现WithXXXX时能利用闭包的特性实现更大程度的灵活性。比如

// 在around上下浮动5%的超时时间
func WithInaccurateTimeout(around time.Duration) Option {
	return func(opt *Options) {
		rate := float64(95 + rand.Intn(11)) / 100
		opt.Timeout = time.Duration(float64(around) * rate)
	}
}

在这个示例中,其实可以直接往Option里传Client:

package client

import (
	"time"
)

const (
	DefaultTimeout  = 1 * time.Second
	DefaultMaxRetry = 3
)

type DemoClient struct {
	host     string
	timeout  time.Duration
	maxRetry int
}

type Option func(opt *DemoClient)

func WithTimeout(timeout time.Duration) Option {
	return func(opt *DemoClient) {
		opt.timeout = timeout
	}
}

func WithMaxRetry(maxRetry int) Option {
	return func(opt *DemoClient) {
		opt.maxRetry = maxRetry
	}
}

func NewDemoClient(host string, opts ...Option) *DemoClient {
	client := &DemoClient{
		host:     host,
		timeout:  DefaultTimeout,
		maxRetry: DefaultMaxRetry,
	}
	for _, o := range opts {
		o(client)
	}
	return client
}

但不是所有时候都可以这么简化,比如有 只在构造时有效,不需要存到对象的字段中的参数 的时候。

结语

选项模式有很多优点,例如:支持传递多个参数,并且在参数发生变化时保持兼容性;支持任意顺序传递参数;支持默认值;方便扩展;通过 WithXXX 的函数命名,可以使参数意义更加明确,等等。

但缺点是,为了实现选项模式,我们显著地增加了很多代码,实际开发中,要根据场景选择是否使用选项模式。

这里只是示例了工厂函数怎么使用选项模式,通常意义上的函数也可以使用,比如 grpc 中的 rpc 方法就是采用选项模式设计的。

选项模式通常适用于以下场景:

  • 结构体参数很多,创建结构体时,我们期望创建一个携带默认值的结构体变量,并选择性修改其中一些参数的值。
  • 结构体参数经常变动,考虑以后的扩展,变动时我们又不想修改创建实例的函数。

如果结构体参数比较少,可以慎重考虑要不要采用选项模式,也许直接用带Option的工厂就足够了。

参考

Functional Options Pattern in Go
golang 设计模式之选项模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值