可接受不定项命名参数的函数
问题
需要对一个对象或者业务实体进行相关的配置,比如:
Go
因为Go不支持重载函数,除了使用像下面这样的不同函数签名来应对不同的配置选项,还有什么简便直观的方法。
type Server struct{
Addr string
Port int
Protocol string
Timeout time.Duration
...
}
Go
func NewDefaultServer(addr string,port int)*Server{
return &Server{addr,port,"tcp",60*time.Second,...}
}
func NewUDPServer(addr string,port int)*Server{
return &Server{addr,port,"udp",60*time.Second,...}
}
func NewServerWithTimeout(addr string,port int,timeout time.Duration)*Server{
return &Server{addr,port,"udp",timeout,...}
}
...
解决方案
Struct Config方法
设置一个配置对象
Go
type Config struct{
Timeout time.Duration
Protocol string
}
于是我们的NewServer()
函数变成了
Go
func NewServer(addr string,port int,conf *Config)*Server{
srv := Server{addr,port} //缺省
srv.Timeout =conf.Timeout
srv.Protocol = conf.Protocol
return &srv
}
调用如下:
Go
如果额外的参数为空,需要置src := NewServer("localhost",8080,&Config{"udp",60*time.Second})
nil
:
Go
src := NewServer("localhost",8080,nil)
Builder模式/Chain-With模式
对于第一种Struct Config
方法,有个不太好的地方就是Config
不是必须的,需要判断nil
或空Config{}
,这会让代码看起来不太干净。如果你熟悉设计模式,一定会想到Builder模式。
Go
type ServerBuilder struct{
Server{}
}
func (sb *ServerBuilder)Create(addr string,port int)*ServerBuilder{
sb.Server.Addr = addr
sb.Server.Port = port
return sb
}
func (sb *ServerBuilder)WithTimeout(timeout time.Duration)(*ServerBuilder){
sb.Server.Timeout = timeout
return sb
}
func (sb *ServerBuilder)WithProtocol(protocol string)(*ServerBuilder){
sb.Server.Protocol = protocol
return sb
}
...
func (sb *ServerBuilder)Build()(Server){
return sb.Server
}
于是就可以用下面的方式调用了:
Go
server:= ServerBuilder{}.Create("localhost",8080).
WithTimeout(60*time.Second).
WithProtocol("tcp").
Build()
上面这样的方式不需要额外的Config类,使用链式调用的方式来构建一个对象。
其中ServerBuilder
也不是必要的,可以在Server
中直接添加Build
方法。增加ServerBuilder
这样一个包装类的优势是:
- 便于处理
error
异常 - 保持
Server
的纯洁干净,无额外的辅助信息和方法
函数式选项
如果真的想省略ServerBuilder
这个包装类,可以使用函数式选项的方法。
首先,先定义一个函数类型
Go
type Option func(*Server)
然后,我们可以使用函数式的方式定义一组如下的函数:
Go
上面这组代码传入一个参数,然后返回一个函数,返回的这个函数会设置自己的func Protocol(p string)Option{
return func(s *Server){
s.Protocol = p
}
}
func Timeout(t timeout.Duration)Option{
return func(s *Server){
s.Timeout = t
}
}
Server
参数。
例如:
当我们调用其中一个函数/方法Protocol("tcp")
时,其返回值是一个func(s *Server){s.Protocol="tcp"}
的函数。
这个叫高阶函数。
现在再定义一个NewServer()
的函数,其中一个可变参数options
可以传入多个上面的函数。然后使用一个for-loop来设置我们的Server
对象。
Go
func NewServer(addr string,port int,options ...Option)(*Server,error){
srv:= Server{
Addr:addr,
Port:port,
}
for _,option := range options{
option(&srv)
}
//...
retrun &srv,nil
}
于是创建Server
对象的时候就可以:
Go
srv1,_ := NewServer("localhost",8080)
srv2,_ := NewServer("localhost",8080,Protocol("tcp"))
srv3,_ := NewServer("localhost",8080,Timeout(60*time.Second))
srv4,_ := NewServer("localhost",8080,Protocol("tcp"),Timeout(60*time.Second))
不需要考虑Config
对象的nil
,也不用Builder
对象,是不是更整洁优雅且语义清楚,像极了Python中的不定命名参数?
在编写正式API的时候,推荐使用这种方式,其至少有下面这些优点:
- 直觉编程
- 高度可配置
- 很容易维护和扩展
- 自文档
- 优雅好看易懂