Golang编程模式:functional options

Mon, Jul 12, 2021 阅读时间 2 分钟

配置选项问题

当我们需要创建一个对象时,往往需要对这个对象的某些成员参数进行配置,比如下面这个结构体:

type Server struct {
    Addr string
    Port int
    Protocol string
    Timeout time.Duration
    MaxConns int
    TLS *tls.Config
}

最简单的方式是直接调用这个结构体,直接填入参数:

s := Server{
    Addr: "127.0.0.1",
    Port: 80,
}

如果并不想对外直接暴露结构体,那么可以写一个工厂方法返回一个*Server对象:

func NewDefaultServer(addr string, port int) (*Server, error) {
 return &Server{
     addr, port, "tcp", 30 * time.Second, 100, nil}, nil 
}

针对不同的配置方式方式,可能需要写多个不同的工厂方法,非常麻烦:

func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) { 
    return &Server{addr, port, "tcp", 30 * time.Second, 100, tls}, nil 
}

func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
    return &Server{addr, port, "tcp", timeout, 100, nil}, nil 
}

解决方式主要有以下几种:

通过配置对象解决配置选项的问题

创建一个关于Server配置的结构体,Server中只保留必要的参数

type Config struct { 
    Protocol string 
    Timeout time.Duration 
    Maxconns int 
    TLS *tls.Config 
}

type Server struct { 
    Addr string 
    Port int 
    Conf *Config 
}

然后编写一个接收必填参数,和可选的Config参数的函数即可

func NewServer(addr string, port int, conf *Config) (*Server, error) {
    if conf != nil{{
         return &Server{addr, port, conf.Protocol, conf.Timeout, conf.Maxconns, conf,TLS}
    }
    return &Server{addr, port}
}

调用时就可以根据情况决定是否传入Config中的参数

s := NewServer("127.0.0.1", 80, nil)

但是这种方式并不是十分优雅。

Builder模式

Builder模式是从java中借鉴过来的,可以通过链式函数调用的方式进行对象的配置。

首先创建一个类(结构体)用来包装Server

type ServerBuilder struct{
    Server
}

然后通过一些方法可以给内部的Server赋值

func (sb *ServerBuilder) Create(addr string, port int) *ServerBuilder {
    sb.Server.Addr = addr 
    sb.Server.Port = port 
    //其它代码设置其它成员的默认值 
    return sb 
}

func (sb *ServerBuilder) WithProtocol(protocol string) *ServerBuilder {
    sb.Server.Protocol = protocol 
    return sb 
}

// 其他参数的方法,都是一个模子

// 最后有一个build方法返回内部赋值过的Server对象
func (sb *ServerBuilder) Build() (Server, err) {
    return sb.Server, nil
}

最后就可以用链式的方式使用了:

s, err := ServerBuilder{}.Create("127.0.0.1", 80).WithProtol("tcp").WithMaxConn(1024).Build()

也可以不使用ServerBuilder对象,或者直接在Server对象上编写这些方法并且错误放在Server结构体中,即Server中加一个Error的参数:

type Server struct{
    ...
    Error error 
}

然后调用方式就成了

s := Server{}.Create("127.0.0.1", 80).WithProtocol("udp").Build()
if s.Error != nil {
    // handle error    
}

这种方式就已经很好了,但还不是这篇文章的主角:functional option。

Functional options模式

首先定义一个Option函数类型,这个函数接收一个*Server对象:

type Option func(*Server)

然后可以使用函数式编程定义一系列函数,他们会接收需要的参数,赋值给*Server对象,然后返回这个Option函数:

func Protocol(p string) Option {
    return func(s *Server) {
         s.Protocol = p 
    } 
}

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

func MaxConns(maxconns int) Option {
     return func(s *Server) {
         s.MaxConns = maxconns 
     } 
}

闭包,实际上就是返回了一个函数,函数中的对象被固定了一个值。然后就可以定义一个NewServer()工厂函数,接收0个或多个很多Option()函数:

func NewServer(addr string, port int, options ...func(*Server)(*Server, err){
    s := Server{
        Addr addr,
        Port: port, 
        Protocol: "tcp",  // 这里可以设置一些默认值
        Timeout: 30 * time.Second, 
        MaxConns: 1000,
        TLS: nil,
    }
    for _, option := range options{
        // 通过这种方式就可以给Server参数赋值了
        option(&s)
    }
    return &s, nil
}

然后就可以使用了,需要哪个参数就传哪个的Option方法即可:

s1 := NewServer("127.0.0.1", 80)
s2 := NewServer("127.0.0.1", 80, Protocol("udp"))
s3 := NewServer("127.0.0.1", 80, Protocol("udp"), Timeout(300 * time.Second))

总结

相比于上述的Config模式和Builder模式,这种方式有以下好处:

  1. 不用纠结空Config时传nil还是空Config{},没有nil的困惑
  2. 不用构建一个Builder的控制对象,完全函数式编程
  3. 直觉式的编程,需要什么就放什么,用起来很舒服
  4. 非常容易拓展,新的参数只需要增加一个闭包函数即可