导语

今天,我们来探讨下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,有不少的属性字段(随便举的例子哈,不一定真实,用来理解下面的编程模式)

  1. Company下必填的属性是Name和ID,除了这两个之外,其他都是选填;
  2. 剩下的字段都是选填,必须有默认值;

我们需要有多种不同的创建 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 还是空)

标签: Go语言

已有 2 条评论

  1. 1 1

    1

  2. 1 1

    555

添加新评论