类固醇的功能选择

哈Ha!我向您介绍MárkSági-Kazár撰写的文章《类固醇功能性选择》 功能选项是Go的典范,适用于干净且可扩展的API。她受到Dave CheneyRob Pike的推崇这篇文章是关于自模板首次引入以来的实践。 功能选项已成为一种通过包含可选参数的配置来创建良好且简洁的API的方法。有很多明显的方法可以做到这一点(构造函数,配置结构,setter等),但是当您需要传递许多选项时,它们的可读性很差,并且不能提供功能选项之类的优质API。

图片





简介-什么是功能选项?


通常,当您创建“对象”时,会使用必要的参数调用构造函数:

obj := New(arg1, arg2)

(让我们忽略以下事实:Go中没有传统的构造

函数。)功能选项允许您使用其他参数扩展API,从而将上面的行变为以下内容:

// I can still do this...
obj := New(arg1, arg2)

// ...but this works too
obj := New(arg1, arg2, myOption1, myOption2)

功能选项基本上是可变函数的参数,该可变函数采用复合或中间配置类型作为参数。由于其可变的性质,即使不使用默认值,也可以不带任何选项地调用构造函数,保持其整洁,这是完全可以接受的。

为了更好地演示该模式,让我们看一个实际的示例(不带功能选项):

type Server struct {
    addr string
}

// NewServer initializes a new Server listening on addr.
func NewServer(addr string) *Server {
    return &Server {
        addr: addr,
    }
}

添加超时选项后,代码如下所示:

type Server struct {
    addr string

    // default: no timeout
    timeout time.Duration
}

// Timeout configures a maximum length of idle connection in Server.
func Timeout(timeout time.Duration) func(*Server) {
    return func(s *Server) {
        s.timeout = timeout
    }
}

// NewServer initializes a new Server listening on addr with optional configuration.
func NewServer(addr string, opts ...func(*Server)) *Server {
    server := &Server {
        addr: addr,
    }

    // apply the list of options to Server
    for _, opt := range opts {
        opt(server)
    }

    return server
}

生成的API易于使用且易于阅读:

// no optional paramters, use defaults
server := NewServer(":8080")

// configure a timeout in addition to the address
server := NewServer(":8080", Timeout(10 * time.Second))

// configure a timeout and TLS in addition to the address
server := NewServer(":8080", Timeout(10 * time.Second), TLS(&TLSConfig{}))


为了进行比较,下面是使用构造函数和config配置结构进行初始化的样子:

// constructor variants
server := NewServer(":8080")
server := NewServerWithTimeout(":8080", 10 * time.Second)
server := NewServerWithTimeoutAndTLS(":8080", 10 * time.Second, &TLSConfig{})


// config struct
server := NewServer(":8080", Config{})
server := NewServer(":8080", Config{ Timeout: 10 * time.Second })
server := NewServer(":8080", Config{ Timeout: 10 * time.Second, TLS: &TLSConfig{} })

使用功能选项而不是构造函数的好处可能很明显:它们更易于维护和读取/写入。当参数没有传递给构造函数时,功能参数也会超出配置结构。在以下各节中,我将显示更多示例,其中配置结构可能不符合预期。

单击简介中的链接,以阅读功能选项的完整历史记录。(译者注-带有Dave Cheney原创文章的博客在俄罗斯并不总是可用,您可能需要通过VPN进行连接才能查看。此外,该文章中的信息也可以在他关于dotGo 2014的报告视频中找到

功能选择实践


函数选项本身仅是传递给构造函数的函数。简单功能的易用性提供了灵活性,并具有很大的潜力。因此,多年来,模板周围出现了许多实践也就不足为奇了。下面,我将列出我认为最流行和有用的做法。

如果您认为缺少某些内容,请给我发送电子邮件。

选项类型


应用功能选项模板时,您要做的第一件事是确定可选功能的类型:

// Option configures a Server.
type Option func(s *Server)

尽管这看起来似乎不是一个很大的改进,但是通过使用类型名称而不是定义函数,代码变得更具可读性:

func Timeout(timeout time.Duration) func(*Server) { /*...*/ }

// reads: a new server accepts an address
//      and a set of functions that accepts the server itself
func NewServer(addr string, opts ...func(s *Server)) *Server

// VS

func Timeout(timeout time.Duration) Option { /*...*/ }

// reads: a new server accepts an address and a set of options
func NewServer(addr string, opts ...Option) *Server

具有选项类型的另一个优点是Godoc将选项函数放在该类型下:

图片

选项类型列表


通常,功能参数用于创建某些事物的单个实例,但并非总是如此。在创建多个实例时重用默认参数列表的情况也不鲜见:

defaultOptions := []Option{Timeout(5 * time.Second)}

server1 := NewServer(":8080", append(defaultOptions, MaxConnections(10))...)

server2 := NewServer(":8080", append(defaultOptions, RateLimit(10, time.Minute))...)

server3 := NewServer(":8080", append(defaultOptions, Timeout(10 * time.Second))...)

这不是可读性很强的代码,尤其是考虑到使用功能选项的目的是创建友好的API。幸运的是,有一种方法可以简化此过程。我们只需要将[]选项直接切片即可:

// Options turns a list of Option instances into an Option.
func Options(opts ...Option) Option {
    return func(s *Server) {
        for _, opt := range opts {
            opt(s)
        }
    }
}

用“选项”功能替换切片之后,上面的代码变为:

defaultOptions := Options(Timeout(5 * time.Second))

server1 := NewServer(":8080", defaultOptions, MaxConnections(10))

server2 := NewServer(":8080", defaultOptions, RateLimit(10, time.Minute))

server3 := NewServer(":8080", defaultOptions, Timeout(10 * time.Second))

使用/设置选项的前缀


选项通常是复杂类型,与超时或最大连接数不同。例如,在服务器软件包中,您可以将Logger接口定义为一个选项(如果没有默认记录器,则可以使用它):

type Logger interface {
    Info(msg string)
    Error(msg string)
}

显然,Logger名称不能用作选项名称,因为接口已经使用它了。您可以调用选项LoggerOption,但这不是一个非常友好的名称。如果您将构造函数视为一个句子,那么在我们的例子中,想到的是:WithLogger。

func WithLogger(logger Logger) Option {
    return func(s *Server) {
        s.logger = logger
    }
}

// reads: create a new server that listens on :8080 with a logger
NewServer(":8080", WithLogger(logger))

复杂类型选项的另一个常见示例是值的切片:

type Server struct {
    // ...

    whitelistIPs []string
}

func WithWhitelistedIP(ip string) Option {
    return func(s *Server) {
        s.whitelistIPs = append(s.whitelistIPs, ip)
    }
}

NewServer(":8080", WithWhitelistedIP("10.0.0.0/8"), WithWhitelistedIP("172.16.0.0/12"))

在这种情况下,该选项通常将值附加到现有值上,而不是覆盖它们。如果需要覆盖一组现有值,则可以在选项名称中使用单词set:

func SetWhitelistedIP(ip string) Option {
    return func(s *Server) {
        s.whitelistIPs = []string{ip}
    }
}

NewServer(
    ":8080",
    WithWhitelistedIP("10.0.0.0/8"),
    WithWhitelistedIP("172.16.0.0/12"),
    SetWhitelistedIP("192.168.0.0/16"), // overwrites any previous values
)

预设模板


特殊用例通常足够通用以支持库中的使用。在配置的情况下,这可能意味着将一组参数分组在一起并用作预设使用。在我们的示例中,服务器可以具有开放式和内部使用方案,以不同的方式设置超时,速度限制,连接数等。

// PublicPreset configures a Server for public usage.
func PublicPreset() Option {
    return Options(
        WithTimeout(10 * time.Second),
        MaxConnections(10),
    )
}

// InternalPreset configures a Server for internal usage.
func InternalPreset() Option {
    return Options(
        WithTimeout(20 * time.Second),
        WithWhitelistedIP("10.0.0.0/8"),
    )
}

尽管预设在某些情况下可能有用,但它们在内部库中可能具有很大的价值,而在公共库中,它们的价值则较小。

默认类型值与默认预设值


Go始终具有默认值。对于数字,通常为零;对于布尔类型,则为false,依此类推。在可选配置中,依靠默认值被认为是一种好习惯。例如,空值应表示无限制的超时而不是不存在(通常是无意义的)。

在某些情况下,默认类型值不是一个好的默认值。例如,Logger的默认值为nil,这可能会导致恐慌(如果您不通过条件检查来保护logger调用)。

在这些情况下,在构造函数中设置值(在应用参数之前)是确定后备的好方法:

func NewServer(addr string, opts ...func(*Server)) *Server {
    server := &Server {
        addr:   addr,
        logger: noopLogger{},
    }

    // apply the list of options to Server
    for _, opt := range opts {
        opt(server)
    }

    return server
}

我看到了带有默认预设的示例(使用上一节中描述的模板)。但是,我认为这不是一个好习惯。这与仅在构造函数中设置默认值相比,表达性要差得多:

func NewServer(addr string, opts ...func(*Server)) *Server {
    server := &Server {
        addr:   addr,
    }

    // what are the defaults?
    opts = append([]Option{DefaultPreset()}, opts...)

    // apply the list of options to Server
    for _, opt := range opts {
        opt(server)
    }

    return server
}

配置结构选项


将Config结构作为功能选项可能并不常见,但是有时会发现这种情况。这个想法是,功能选项是指配置结构,而不是指正在创建的实际对象:

type Config struct {
    Timeout time.Duration
}

type Option func(c *Config)

type Server struct {
    // ...

    config Config
}

当您有许多选项时,此模板很有用,并且创建配置结构似乎比在函数调用中列出所有选项更干净:

config := Config{
    Timeout: 10 * time.Second
    // ...
    // lots of other options
}

NewServer(":8080", WithConfig(config))

此模板的另一个用例是设置默认值:

config := Config{
    Timeout: 10 * time.Second
    // ...
    // lots of other options
}

NewServer(":8080", WithConfig(config), WithTimeout(20 * time.Second))

进阶范本


在编写了数十个功能选项之后,您可能会想知道是否有更好的方法可以做到这一点。不是在使用方面,而是在长期支持方面。

例如,如果我们可以定义类型并将其用作参数该怎么办:

type Timeout time.Duration

NewServer(":8080", Timeout(time.Minute))

(请注意,API的使用保持不变)

事实证明,通过更改Option类型,我们可以轻松地做到这一点:

// Option configures a Server.
type Option interface {
    // apply is unexported,
    // so only the current package can implement this interface.
    apply(s *Server)
}

将可选功能重写为接口为实现功能选项的许多新方法打开了大门:

各种内置类型可用作没有包装函数的参数:

// Timeout configures a maximum length of idle connection in Server.
type Timeout time.Duration

func (t Timeout) apply(s *Server) {
    s.timeout = time.Duration(t)
}

选项和配置结构的列表(请参阅前面的部分)也可以重新定义如下:

// Options turns a list of Option instances into an Option.
type Options []Option

func (o Options) apply(s *Server) {
    for _, opt := range o {
        o.apply(s)
    }
}

type Config struct {
    Timeout time.Duration
}

func (c Config) apply(s *Server) {
    s.config = c
}

我个人最喜欢的是能够在多个构造函数中重用该选项的功能:

// ServerOption configures a Server.
type ServerOption interface {
    applyServer(s *Server)
}

// ClientOption configures a Client.
type ClientOption interface {
    applyClient(c *Client)
}

// Option configures a Server or a Client.
type Option interface {
    ServerOption
    ClientOption
}

func WithLogger(logger Logger) Option {
    return withLogger{logger}
}

type withLogger struct {
    logger Logger
}

func (o withLogger) applyServer(s *Server) {
    s.logger = o.logger
}

func (o withLogger) applyClient(c *Client) {
    c.logger = o.logger
}

NewServer(":8080", WithLogger(logger))
NewClient("http://localhost:8080", WithLogger(logger))

摘要


功能参数是用于创建具有数十个参数的整洁且可扩展的API的强大模板。尽管与支持简单的配置结构相比,它所做的工作要多一些,但与替代方案相比,它提供了更大的灵活性并提供了更简洁的API。

All Articles