Options 模式

标签:设计模式首次发布:2024-04-16最近修改:2024-04-16

在软件开发领域,Options 模式(Options Pattern)是一种常见的设计模式,它允许用户通过提供一系列选项来自定义函数、类型或对象的行为。在 Golang 中,选项模式的应用非常广泛,尤其是在库和框架的设计中。接下来将深入探讨 Golang 中选项模式的实现方式,以及如何利用选项模式提高代码的灵活性和可维护性。

重新认识 type 关键字

type 是 go 语法里的重要而且常用的关键字。搞清楚 type 的使用,就容易理解 go 语言中的核心概念 struct、interface、函数等的使用。

类型定义

使用 type 可以定义结构体、接口、函数,以及自定义类型:

go
type Preson struct {    name string    age int}type USB interface {    start()    end()}type function func() inttype Duration int64

可以看到都是通过语法:type "自定义类型名称" "基于已有类型或者结构定义"进行类型的定义。那么以type Duration int64为例,Duration 和 int64 之间是什么关系呢?

其实 Duration 类型相比于 int64 类型, Duration 除了能表示 int64 类型所表示的数值,还具有了一定的语义再里面。

  • 一个比较贴切的例子来说明 Duration 和 int64 之间的关系,可以用度量单位和数字之间的关系来比喻。

  • 假如你有一个数字 60。这个数字本身没有特定的含义,它只是一个数值。它可以代表分钟、秒钟、天数,或者仅仅是一个数量。这就像 int64 类型,它是一个基本的数据类型,可以用来存储任何数值,但它本身不提供任何关于这个数值用途的上下文信息。

  • 现在,如果我们说这个 60 是分钟,那么它突然有了具体的时间含义。这就类似于把 int64 转换为 Duration 类型。通过明确这是 60 分钟,我们用一个具体的单位(分钟)赋予了这个数字一个特定的上下文和用途。Duration 作为一种类型,它带有时间的语义,告诉我们这不仅仅是一个数字,而是一个时间间隔。

通过 type 定义新类型的这种做法通常用于增强类型安全,使代码更清晰、易于维护,并且可以附加特定于该类型的方法。这样的新类型并不会继承原始类型的方法,也不能隐式转换。

当声明 type Duration int64 时,Duration 成为了 int64 的一个新的类型,但它们在 Go 语言的类型系统中是不兼容的。尽管 Duration 底层使用的是 int64 的存储方式,但你不能直接将一个 int64 类型的变量赋值给一个 Duration 类型的变量,反之亦然。它们之间的赋值都需要显式类型转换。

go
type Duration int64var time Durationvar num int64time = Duration(num)num = int64(time)

类型别名

go
type TypeAlias = Type

类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。也就是两种类型完全等价,没有任何区别。下面请看 Demo:

go
// Address 结构体表示一个地址type Address struct {    Street     string    PostalCode int}type HomeAddress = Addresstype WorkAddress = Address// PrintHomeAddress 打印家庭地址的方法func (h *HomeAddress) PrintHomeAddress() {    fmt.Printf("%s, %d\n", h.Street, h.PostalCode)}// PrintWorkAddress 打印工作地址的方法func (w *WorkAddress) PrintWorkAddress() {    fmt.Printf("%s, %d\n", w.Street, w.PostalCode)}func main() {    home := HomeAddress{        Street:     "123 Maple St",        PostalCode: 90210,    }    work := WorkAddress{        Street:     "456 Oak St",        PostalCode: 10001,    }          // 两个类型完全等价,可以互相调用对应定义的方法    home.PrintWorkAddress()    work.PrintHomeAddress()}

函数选项

Demo 示例

函数选项是一种通过函数参数来传递选项信息的方式。这种方式可以使代码更加清晰和易于扩展,同时提供了更灵活的定制能力。下面是一个简单的连接 redis 的 Demo 代码,使用函数选项来设置一些非必要的连接参数。

go
import "fmt"// ClientOptions 中的network,address为必填type ClientOptions struct {    maxIdleConns int    timeout      int    wait         bool}type ClientOption func(c *ClientOptions)// WithMaxIdleConns 设置最大空闲连接数func WithMaxIdleConns(maxIdleConns int) ClientOption {    return func(c *ClientOptions) {        c.maxIdleConns = maxIdleConns    }}// WithTimeout 设置连接池释放连接的超时连接func WithTimeout(timeout int) ClientOption {    return func(c *ClientOptions) {        c.timeout = timeout    }}// WithWaitMode 设置是否阻塞func WithWaitMode(wait bool) ClientOption {    return func(c *ClientOptions) {        c.wait = wait    }}// Client 表示 Redis 客户端type Client struct {    network string    address string    options *ClientOptions}// NewClient 新建客户端, 适用于简单或标准的Redis连接需求func NewClient(network, address string, opts ...ClientOption) *Client {    if network == "" || address == "" {        return nil    }    options := &ClientOptions{}    for _, opt := range opts {        opt(options)    }    return &Client{        network: network,        address: address,        options: options,    }}func main() {    client := NewClient("tcp", "localhost:6379", WithMaxIdleConns(10), WithTimeout(300), WithWaitMode(true))    if client == nil {        panic("client is nil")    }    fmt.Println(client.options.maxIdleConns)    fmt.Println(client.options.timeout)    fmt.Println(client.options.wait)}

运行结果如下:

text
PS D:\GoProjects\main> go run main.go10300true

主要是 NewClient 函数内部:

  • 该函数的opts参数是一个[]ClientOption类型的切片,而 ClientOption 类型则是一个func(c *ClientOptions)的函数类型。
  • 通过遍历 opts 切片得到 opt,那么 opt 就是func(c *ClientOptions)函数。在调用 WithWaitMode(true)的时候,其实就是调用函数func(c *ClientOptions),该函数会对 options 指针指向的 ClientOptions 结构体实例进行操作,从而修改某一个字段。

优点:

  1. 灵活性和可扩展性:函数选项模式允许开发者以增量的方式添加或修改配置,而不需要更改已有的函数签名或调用方式。这使得在不破坏现有代码的前提下增加新的选项成为可能。
  2. 可读性和清晰性:选项通过命名函数提供,这使得调用代码更易读和理解。例如,WithMaxIdleConns(10) 比简单的传递一个数字更明确表达了配置的意图。
  3. 封装性:选项函数内部可以包含复杂的逻辑来设置对象的状态,而这些逻辑对于使用者是隐藏的。这有助于封装配置细节,使得外部代码不需要关心如何正确设置对象状态。

第三方源码(ETCD)

下面是etcd(github.com)的底层源码:

go
func NewCtxClient(ctx context.Context, opts ...Option) *Client {    cctx, cancel := context.WithCancel(ctx)    c := &Client{ctx: cctx, cancel: cancel, lgMu: new(sync.RWMutex)}    for _, opt := range opts {        opt(c)    }    if c.lg == nil {        c.lg = zap.NewNop()    }    return c}type Option func(*Client)func WithZapLogger(lg *zap.Logger) Option {    return func(c *Client) {        c.lg = lg    }}

可以看到使用的是典型的函数选项模式来实现日志的相关设置。

接口选项

Demo 示例

go
type ClientOptions struct {    maxIdleConns int    timeout      int    wait         bool}// ClientOption 定义了一个可以应用于ClientOptions的接口type ClientOption interface {    apply(*ClientOptions)}// MaxIdleConnsOption 设置最大空闲连接数type MaxIdleConnsOption struct {    MaxIdleConns int}// MaxIdleConnsOption 实现ClientOption接口func (m *MaxIdleConnsOption) apply(opts *ClientOptions) {    opts.maxIdleConns = m.MaxIdleConns}// WithMaxIdleConns 设置最大空闲连接数func WithMaxIdleConns(maxIdleConns int) *MaxIdleConnsOption {    return &MaxIdleConnsOption{MaxIdleConns: maxIdleConns}}// TimeoutOption 设置连接池释放连接的超时连接type TimeoutOption struct {    Timeout int}// TimeoutOption 实现ClientOption接口func (t *TimeoutOption) apply(opts *ClientOptions) {    opts.timeout = t.Timeout}// WithTimeout 设置连接池释放连接的超时连接func WithTimeout(timeout int) *TimeoutOption {    return &TimeoutOption{Timeout: timeout}}// WaitModeOption 设置是否阻塞type WaitModeOption struct {    WaitMode bool}// WaitModeOption 实现ClientOption接口func (w *WaitModeOption) apply(opts *ClientOptions) {    opts.wait = w.WaitMode}// WithWaitMode 设置是否阻塞func WithWaitMode(wait bool) *WaitModeOption {    return &WaitModeOption{WaitMode: wait}}// Client 表示一个Redis客户端type Client struct {    network string    address string    options *ClientOptions}// NewClient 新建客户端, 适用于简单或标准的Redis连接需求func NewClient(network, address string, opts ...interface{}) *Client {    if network == "" || address == "" {        return nil    }        options := &ClientOptions{}    for _, opt := range opts {        switch o := opt.(type) {        case ClientOption:            o.apply(options)        }    }    return &Client{        network: network,        address: address,        options: options,    }}func main() {    client := NewClient("tcp", "localhost:6379", WithMaxIdleConns(10), WithTimeout(300), WithWaitMode(true))    if client == nil {        panic("client is nil")    }    fmt.Println(client.options.maxIdleConns)    fmt.Println(client.options.timeout)    fmt.Println(client.options.wait)}

运行结果如下:

text
PS D:\GoProjects\main> go run main.go10300true

在这段代码当中:

  • ClientOptions 是一个简单的结构体,用于存储配置选项。

  • ClientOption 接口定义了一个名为 apply 的方法,该方法接受一个 *ClientOptions 类型的参数。如果某个类型想要实现该接口,那么必须提供 apply 方法的具体实现(鸭子模型:如果一个动物实现了鸭子叫的方法,那么就可以认为这个动物是一只鸭子)。这个设计模式的核心思想是:通过接口将修改配置的行为抽象出来,使得不同的配置选项可以以统一的方式应用到 ClientOptions 实例上。

  • MaxIdleConnsOption 是一个结构体,它实现了 ClientOption 接口。该结构体的字段 MaxIdleConns用于存储最大空闲连接数的值。它的 apply 方法直接修改传入的 ClientOptions 实例,设置 maxIdleConns 字段的值。

  • WithMaxIdleConns 函数是一个工厂函数,它接受一个整数作为参数,并返回一个 ClientOption 接口。这个函数构造一个 MaxIdleConnsOption 实例,并通过该实例的指针返回,该指针满足 ClientOption 接口的要求。

这种方式通过接口ClientOption实现了多态。在NewClient函数中,它们都能接受一系列ClientOption接口类型的参数,而这些参数实际上可以是任何实现了ClientOption接口的类型。这使得NewClient函数能够以统一的方式处理不同的配置选项,而无需知道配置选项的具体类型和实现细节。

通过这种设计,可以构建一个灵活且易于扩展的配置系统,这在创建需要处理多种配置场景的复杂系统时非常有用。希望这个解释有助于您更好地理解代码的结构和背后的设计理念。

第三方源码(gRPC)

go
type ServerOption interface {    apply(*serverOptions)}type funcServerOption struct {    f func(*serverOptions)}// funcServerOption 实现ServerOption接口func (fdo *funcServerOption) apply(do *serverOptions) {    fdo.f(do)}// newFuncServerOption 是一个工厂函数。作用是封装任何给定的函数为一个 ServerOption,从而使它可以被用作配置选项。func newFuncServerOption(f func(*serverOptions)) *funcServerOption {    return &funcServerOption{        f: f,    }}func SharedWriteBuffer(val bool) ServerOption {    return newFuncServerOption(func(o *serverOptions) {        o.sharedWriteBuffer = val    })}func WriteBufferSize(s int) ServerOption {    return newFuncServerOption(func(o *serverOptions) {        o.writeBufferSize = s    })}func ReadBufferSize(s int) ServerOption {    return newFuncServerOption(func(o *serverOptions) {        o.readBufferSize = s    })}

这种模式非常适用于需要多个可选配置参数的情况,特别是在对象可能有多种配置组合时。通过使用这种模式,gRPC 确保了其 API 的灵活性和扩展性,同时也提高了代码的维护性和可读性。

总结:

  • 函数 Options:适用于需要配置一组相关的参数的情况。
  • 接口 Options:适用于需要定义多态行为的情况。