golang中的选项模式

liuyuede上海

索引

https://waterflow.link/articles/1663835071801

当我在使用go-zero时,我看到了好多像下面这样的代码:

...

type (
    // RunOption defines the method to customize a Server.
    RunOption func(*Server)

    // A Server is a http server.
    Server struct {
        ngin   *engine
        router httpx.Router
    }
)

...

// AddRoutes add given routes into the Server.
func (s *Server) AddRoutes(rs []Route, opts ...RouteOption) {
    r := featuredRoutes{
        routes: rs,
    }
    for _, opt := range opts {
        opt(&r)
    }
    s.ngin.addRoutes(r)
}

...

// WithJwt returns a func to enable jwt authentication in given route.
func WithJwt(secret string) RouteOption {
    return func(r *featuredRoutes) {
        validateSecret(secret)
        r.jwt.enabled = true
        r.jwt.secret = secret
    }
}

我们可以把重点放在RouteOption上面。这里就使用了选项模式。

什么是选项模式?

选项模式是一种函数式编程模式,用于为可用于修改其行为的函数提供可选参数。

如果你用过php的话,你肯定会看到过这样的函数,毕竟PHP是世界上最好的语言:

public function cache(callable $callable, $duration = null, $dependency = null)
  
// 我们可以这样调用
cache($callable);

// 也可以把后面的可选参数带上
cache($callable, $duration);

这种能力在 API 设计中非常有用,因为

  • 允许用户以最低配置使用方法,同时仍为有经验的用户提供充足的配置选项。
  • 允许开发者在不破坏向后兼容性的情况下添加新选项。

然而,在 Golang 中,这是不可能的。该语言不提供添加可选参数的方法。

这就是“选项模式”的用武之地。它允许用户在调用方法 时传递其他选项,然后可以相应地修改其行为。让我们看一个小例子。

假设我们有一个结构体Server,它有 3 个属性port, timeoutmaxConnections

type Server struct {
    port           string
    timeout        time.Duration
    maxConnections int
}

然后我们有个Server的工厂方法

func NewServer(port string, timeout time.Duration, maxConnections int) *Server {
    return &Server{
        port:           port,
        timeout:        timeout,
        maxConnections: maxConnections,
    }
}

但是现在我们希望只有端口号是必传的,timeoutmaxConnections成为可选参数。在很多情况下,这些的默认值就足够了。另外,我不想用这些不必要的配置来轰炸刚刚学习或试验我的 方法 的新用户。让我们看看该怎么去实现。

首先我们定义一个新的option结构体

type Option func(*Server)

Option是一个函数类型,它接受指向我们的Server. 这很重要,因为我们将使用这些选项修改我们的Server实例。

现在让我们定义我们的选项。惯例是在我们的选项前面加上 With,但可以随意选择适合您的域语言的任何名称

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithMaxConnections(maxConn int) Option {
    return func(s *Server) {
        s.maxConnections = maxConn
    }
}

我们的两个选项WithTimeoutWithMaxConnections,采用配置值并返回一个Option。这Option只是一个函数,它接受一个指向我们Server对象的指针并将所需的属性设置为提供的值。例如,WithTimeout获取超时持续时间,然后返回一个函数(其签名与 Option相同)将我们服务器的 timeout 属性设置为提供的值。

在这里,我们使用了一种几乎所有现代语言(包括 Golang)都支持的称为闭包的技术

我们的工厂方法Server现在需要修改以支持这种变化

func NewServer(port string, options ...Option) *Server {
    server := &Server{
        port: port,
    }

    for _, option := range options {
        option(server)
    }

    return server
}

现在我们就可以用上面世界上最好的语言的方式调用了

NewServer("8430")
NewServer("8430", WithTimeout(10*time.Second))
NewServer("8430", WithTimeout(10*time.Second), WithMaxConnections(10))

在这里你可以看到我们的客户端现在可以创建一个只有端口的最小服务器,但如果需要也可以自由地提供更多的配置选项。

这种设计具有高度的可扩展性和可维护性,甚至比我们在 PHP 中看到的可选参数还要好。它允许我们添加更多选项,而不会膨胀我们的函数签名,也不会触及我们在工厂方法中的代码。

下面是完整代码:

package main

import "time"

type Option func(*Server)

type Server struct {
    port           string
    timeout        time.Duration
    maxConnections int
}

func NewServer(port string, options ...Option) *Server {
    server := &Server{
        port: port,
    }

    for _, option := range options {
        option(server)
    }

    return server
}

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithMaxConnections(maxConn int) Option {
    return func(s *Server) {
        s.maxConnections = maxConn
    }
}

func main() {
    NewServer("8430")
    NewServer("8430", WithTimeout(10*time.Second))
    NewServer("8430", WithTimeout(10*time.Second), WithMaxConnections(10))
}
阅读 571
15 声望
0 粉丝
0 条评论
15 声望
0 粉丝
宣传栏