Functional Options Pattern in Go

Functional Options Pattern:

  1. 定义一个Options结构体(StuffClientOptions),包含所有的可选项;
  2. 定义一个函数类型,参数是Options结构指针(StuffClientOption);
  3. 创建一个Options类型的变量,包含所有的默认值(defaultStuffClientOptions);
  4. 对所有的可选项定义设置函数(WithXXX),在里面对选项进行设置;
  5. 定义一个接口,指定我们提供的服务(StuffClient);
  6. 创建一个包内可见的变量,实现接口(stuffClient);
  7. 定义一个构造函数,使用可变参数(NewStuffClient)。

Go语言让程序员困扰的一个问题就是如何编写一个可选参数的函数。当一个类具有多个选项,我们在构造这个类的时候可能希望一些选项使用默认值,而有时候还需要进行更精细的控制。

这个在很多语言中都很容易。比如在C类语言中,我们可以通过函数重载的方式,提供具有不同个数参数的同名字函数;在PHP中可以对参数设置一个默认值。但是在Go中这些方法都行不通。

这个时候就可以用功能选项模式(Functional Options Pattern)来完成。这里通过一个简单的例子逐步介绍这个模式。

假如我们提供一个服务叫做StuffClient,这个服务有两个可选项(超时timeout和重试次数retries):

type StuffClient interface {
    DoStuff() error
}

type stuffClient struct {
    conn Connection
    timeout int
    retires int
}

这个结构体是私有的,所以我们需要一个构造函数:

func NewStuffClient(conn Connection, timeout, retries int) StuffClient {
    return &stuffClient {
        conn: conn,
        timeout: timeout,
        retries: retries,
    }
}

但是这样每次构造一个新的对象都需要传递全部参数,但是大多数时候我们使用默认值就可以了。我们同样不能定义多个具有相同名字但不同参数的函数,然后回报redelared错误。

这时我们可以定义一个不同的构造函数:

func NewStuffClient(conn Connection) StuffClient {
    return &stuffClient {
        conn: conn,
        timeout: DEFAULT_TIMEOUT,
        retries: DEFAULT_RETRIES,
    }
}

func NewStuffClientWithOptions(conn Connection, timeout, retries int) StuffClient {
    return &stuffClient {
        conn: conn,
        timeout: timeout,
        retries: retries,
    }
}

但是这样太麻烦了,尤其当我们需要再添加一个选项的时候,需要改动的地方太多了。

我们可以传递一个配置对象config:

type StuffClientOptions struct {
    Retries int // number of times to retry the request before giving up
    Timeout int // connection timeout in seconds
}

func NewStuffClient(conn Connection, options StuffClientOptions) StuffClient {
    return &stuffClient {
        conn: conn,
        timeout: options.Timeout,
        retries: options.Retries,
    }
}

但是这样也有点麻烦,我们需要每次都要构造一个config然后传进去,即使我们不想指定某些值。

最后我们的功能选项模式出场了。通过Go的闭包,可以更加简洁地实现上面的需求:

type StuffClientOption func(*StuffClientOptions)

type StuffClientOptions struct {
    Retries int // number of times to retry the request before giving up
    Timeout int // connection timeout in seconds
}

func WithRetries(r int) StuffClientOption {
    return func(o *StuffClientOptions) {
        o.Retries = r
    }
}

func WithTimeout(t int) StuffClientOption {
    return func(o *StuffClientOptions) {
        o.Timeout = t
    }
}

首先,我们为我们的结构StuffClient定义了一个可选项的结构StuffClientOptions,又定义了一个函数类型StuffClientOption并以上面的Option结构作为参数。然后还有几个WithXXX函数返回一个闭包。现在,我们可以这样:

var defaultStuffClientOptions = StuffClientOptions {
    Retries: 3,
    Timeout: 2,
}

func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
    options := defaultStuffClientOptions
    for _, o := range opts {
        o(&options)
    }
    return &stuffClient {
        conn: conn,
        timeout: options.Timeout,
        retries: options.Retries,
    }
}

这里我们调整了构造函数的定义,接收一个可变参数,这个可变参数是上面函数类型的数组,然后将所有的函数执行一遍,就对我们的选项进行了设置,没有设置的使用默认值。

完整的代码:

package main

import (
    "fmt"
)

// 1、定义一个Option结构体,包含所有需要的可选项
type StuffClientOptions struct {
    Retries int // # of times to retry the request before giving up
    Timeout int // connection timeout in seconds
}

// 2、定义一个函数类型,这个函数的参数是Option结构体的指针类型
type StuffClientOption func(*StuffClientOptions)

// 3、创建一个Option类型的变量,包括所有的默认值
var defaultStuffClientOptions = StuffClientOptions {
    Retries: 3,
    Timeout: 2,
}

// 4、WithXXX函数,参数是选项的值,返回2中定义的函数,内部进行设置
func WithRetries(r int) StuffClientOption {
    return func(o *StuffClientOptions) {
        o.Retries = r
    }
}

func WithTimeout(t int) StuffClientOption {
    return func(o *StuffClientOptions) {
        o.Timeout = t
    }
}

// 5、定义接口
type StuffClient interface {
    DoStuff() error
}

type Connection struct{}

// 6、包内可见的一个变量
type stuffClient struct {
    conn Connection
    timeout int
    retries int
}

// 构造函数
func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
    options := defaultStuffClientOptions
    for _, o := range opts {
        o(&options)
    }

    return &stuffClient {
        conn: conn,
        timeout: options.Timeout,
        retries: options.Retries,
    }
}

func (c stuffClient) DoStuff() error {
    return nil
}

func main() {
    x := NewStuffClient(Connection{})
    fmt.Printf("%+v\n", x) // print &{conn:{} timeout:2 retries:3}

    x = NewStuffClient(
        Connection{},
        WithRetries(1),
    )
    fmt.Printf("%+v\n", x) // print &{conn:{} timeout:2 retries:1}

    x = NewStuffClient(
        Connection{},
        WithRetries(1),
        WithTimeout(100),
    )
    fmt.Printf("%+v\n", x) // print &{conn:{} timeout:100 retries:1}
}

更简单一点,我们可以不定义StuffClientOptions,直接使用我们的StuffClient

var defaultStuffClient = struffClient {
    retries: 3,
    timeout: 2,
}

type StuffClientOption func(*stuffClient)

func WithRetries(r int) StuffClientOption {
    return func(o *stuffClient) {
        o.retries = r
    }
}

func WithTimeout(t int) StuffClientOption {
    return func(o *stuffClient) {
        o.timeout = t
    }
}

type StuffClient interface {
    DoStuff() error
}

type stuffClient struct {
    conn Connection,
    timeout int
    retries int
}

type Connection struct{}

func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
    client := defaultStuffClient
    for _, o := range opts {
        o(&client)
    }

    client.conn = conn
    return client
}

func (c stuffClient) DoStuff() error {
    return nil
}

这里没有使用config结构,但是在一些场景中使用config结构更加合适,比如在构造函数中对config有进一步的操作但是并不保存数据,或者把这个config传递到其他地方的时候,使用config结构更加方便。


 Previous
Data Science (Part 1): Basic R Data Science (Part 1): Basic R
1. 开始吧1.1 安装包与导入包# installing the dslabs package install.packages("dslabs") # loading the dslabs package into the R ses
2020-09-26
Next 
ddia, Distributed Data (Part 4): Troubles ddia, Distributed Data (Part 4): Troubles
这篇文章是ddia第八章的阅读笔记。 0x00 Intro 所有可能出错的地方一定会出错。 这篇文章来讲讲,分布式系统中会出现哪些错误。 知道会有什么错误,才能知道如何去解决。 0x01 Faults and Partial Fail
2020-07-27
  You Will See...