谈谈Go语言:函数式选项模式
导语
今天,我们来探讨下Go编程模式-函数式选项模式,英文叫Functional Optional。这是一个函数式编程的应用案例,目前在Go语言中比较流行的一种编程模式。某些业务对象在初始化时,如果属性多,大部分属性没有默认值,初始化的函数会非常冗长,代码也不好维护,下面就来看下函数式选项模式(Functional Optional)怎么解决这个问题。
业务对象的初始化
在实际的业务开发过程中,我们会经常性地需要对一个对象(或是业务实体)进行初始化。比如下面这个业务实体Company,又很多待初始化的列表:
type Company struct {
Name string
ID string
Owner string
Addr string
Industry string
Product string
License string
Date string
}
针对上面业务实体Company,有不少的属性字段(随便举的例子哈,不一定真实,用来理解下面的编程模式)
- Company下必填的属性是Name和ID,除了这两个之外,其他都是选填;
- 剩下的字段都是选填,必须有默认值;
我们需要有多种不同的创建 Company 的函数签名,如下所示:
func NewDefaultCompany(name string, id string) (*Company, error) {
return &Company{name, id, "none", "china", "all", "none", "none", "none"}, nil
}
func NewCompanyWithOwner(name string, id string, owner string) (*Company, error) {
return &Company{name, id, owner, "china", "all", "none", "none", "none"}, nil
}
func NewCompanyWithIndustry(name string, id string, owner string, industry string) (*Company, error) {
return &Company{name, id, owner, "china", industry, "none", "none", "none"}, nil
}
func NewCompanyFull(name string, id string, owner string, addr string, industry string, product string, license string, date string) (*Company, error) {
return &Company{name, id, owner, addr, industry, product, license, date}, nil
}
因为Go语言不支持重载,所以看到实际研发过程中,就会产生过多的签名,久而久之,代码的维护成本,需要初始化业务对象时,还得仔细看,究竟使用哪一个比较合理,或者需要再增加一个函数签名。
显然,上面的编码风格是很差的,要解决上面的问题,最常见的方法就是再抽象一个配置对象,比如:
type Config struct {
Owner string
Addr string
Industry string
Product string
License string
Date string
}
我们把那些非必输的选项都移到一个结构体里,于是 Company 对象变成了:
type Company struct {
Name string
ID string
Conf *Config
}
创建 Company 对象的函数签名可以收敛为一个:
func NewCompany(name string, id string, conf *Config) (*Company, error) {
//...
}
这段代码算是不错了,大多数情况下,我们可能就止步于此了。但是,对于有洁癖的有追求的程序员来说,他们能看到其中有一点不好的是,Config 并不是必需的,所以,你需要判断是否是 nil 或是 Empty – Config{}这让我们的代码感觉还是有点不是很干净。
Builder模式
Builder设计模式是可以解决上面引入 Config 之后仍然存在的问题,引入Builder模式后,写出来的代码如下:
Company company = new Company.Builder()
.name("ABC")
.id("1")
.owner("zhang san")
.addr("china")
.industry("all")
.product("none")
.license("1")
.date("2020.2.2")
.build();
仿照上面这个模式,我们可以把上面代码改写成如下的代码(注:下面的代码没有考虑出错处理,其中关于出错处理的更多内容,请参看《谈谈Go语言:出错处理》)
type CompanyBuilder struct {
Company
}
func (cb *CompanyBuilder) Create(name string, id string) *CompanyBuilder {
cb.Company.Name = name
cb.Company.ID = id
return cb
}
func (cb *CompanyBuilder) WithOwner(owner string) *CompanyBuilder {
cb.Company.Owner = owner
return cb
}
func (cb *CompanyBuilder) WithIndustry(industry string) *CompanyBuilder {
cb.Company.Industry = industry
return cb
}
// 其他属性类似实现
……
func (cb *CompanyBuilder) Build() (Company) {
return cb.Company
}
如此,就可以使用最上面的写法去初始化 Company 对象了,上面这样的方式也很清楚,不需要额外的 Config 类,使用链式的函数调用的方式来构造一个对象,只需要多加一个 CompanyBuilder 类,这个 CompanyBuilder 类似乎有点多余,我们似乎可以直接在 Company 上进行这样的 CompanyBuilder 构造,的确是这样的。但是在处理错误的时候可能就有点麻烦(需要为 Company 结构增加一个 error 成员,破坏了 Company 结构体的“纯洁”),不如一个包装类更好一些。
如果我们想省掉这个包装的结构体,那么就轮到我们的Functional Options上场了,函数式编程。
Functional Option
首先,定义一个函数类型:
type Option func(*Company)
然后,使用函数式编程的方式定义一组这样的初始化函数:
func Owner(owner string) Option {
return func(c *Company) {
c.Owner = owner
}
}
func Addr(addr string) Option {
return func(c *Company) {
c.Addr = addr
}
}
func Industry(industry string) Option {
return func(c *Company) {
c.Industry = industry
}
}
func Product(product string) Option {
return func(c *Company) {
c.Product = product
}
}
func License(license string) Option {
return func(c *Company) {
c.License = license
}
}
func Date(date string) Option {
return func(c *Company) {
c.Date = date
}
}
上面这组代码传入一个参数,然后返回一个函数,返回的这个函数会设置自己的 Company 参数。例如:当我们调用其中的一个函数用 Owner(“li si”) 时,其返回值是一个 func(c* Company) { c.Owner = “li si” } 的函数。
好了,现在我们再定一个 NewCompany()的函数,其中,有一个可变参数 options 其可以传出多个上面上的函数,然后使用一个for-loop来设置我们的 Company 对象。
func NewCompany(name string, id string, options ...func(*Company)) (*Company, error) {
company := Company{
Name: name,
ID: id,
Owner: "zhang san",
Addr: "china",
Industry: "all",
Product: "none",
License: "",
Date: "2020.2.2",
}
for _, option := range options {
option(&company)
}
//...
return &company, nil
}
于是,创建一个 Company 对象的时候,使用方法如下:
company,_ := NewCompany("ABC", "1", Owner("li si"))
怎么样,是不是高度的整洁和优雅?不但解决了使用 Config 对象方式 的需要有一个config参数,但在不需要的时候,是放 nil 还是放 Config{}的选择困难,也不需要引用一个 CompanyBuilder 的控制对象,直接使用函数式编程的试,在代码阅读上也很优雅。
所以,以后,大家在要玩类似的代码时,强烈推荐使用Functional Options这种方式,这种方式至少带来了如下的好处:
- 直觉式的编程
- 高度的可配置化
- 很容易维护和扩展
- 自文档,整个初始化的过程是按属性解耦的,不是按属性组合的
- 对于新来的人很容易上手
- 没有什么令人困惑的事(是nil 还是空)