Go 函数式编程 (Functional Options)

138 阅读3分钟

配置选项问题

先从创建一个Person说起,有如下一个Person对象,

type Person struct {
   Name string
   Age  int

   Weight float32
   Addr   string
}

拥有三个属性,NameAge, Addr, 其中NameAge必填且不能为空,Addr 为可选属性。

可以很容易的写出如下的构造代码。

func NewPersonWithNameAndAge(name string, age int) *Person {
   return &Person{Name: name, Age: age}
}

func NewPersonWithNameAndAgeAndAndWeight(name string, age int, weight float32) *Person {
   person := NewPersonWithNameAndAge(name, age)
   person.Weight = weight
   return person
}
func NewPersonWithNameAndAgeAndWeightAndAddr(name string, age int, weight float32, addr string) *Person {
   person := NewPersonWithNameAndAgeAndAndWeight(name, age, weight)
   person.Addr = addr
   return person
}

因为go 不支持函数重载,不得不使用不同的函数名来处理。

配置对象

通常可以很容易想到对可选配置封装成一个配置对象

type Config struct {
   Weight float32
   Addr   string
}

然后Person对象变成这样

type Person struct {
   Name string
   Age  int

   Config *Config
}

这样我们就可以使用一个NewPerson构造函数来新建对象了,在对可选属性,新建一个配置对象来填充。

func NewPerson(name string, age int, config *Config) *Person {
   return &Person{
      Name:   name,
      Age:    age,
      Config: config,
   }
}
person1 := NewPerson("1", 1, nil)

config := &Config{
   Weight: 0,
   Addr:   "",
}
person2 := NewPerson("2", 2, config)

虽然这么做可以解决多个构造函数的问题,但是Config对象对于Person来说是多余的,并不需要。甚至于在使用可选属性的时候还需要判断 Config 是否为nil

Builder 模式

如果对Java 熟悉的可以进一步想到使用建造者模式重构。

Person p = new Person.Builder() 
    .name("aa")
    .age(1)
    .build();

类似的, 我们可以在go中重构成如下代码

type PersonBuilder struct {
   Person
}

func (pb *PersonBuilder) Create(name string, age int) *PersonBuilder {
   pb.Person.Name = name
   pb.Person.Age = age
   return pb
}

func (pb *PersonBuilder) WithWeight(weight float32) *PersonBuilder {
   pb.Person.Weight = weight
   return pb
}
func (pb *PersonBuilder) WithAddr(addr string) *PersonBuilder {
   pb.Person.Addr = addr
   return pb
}

func (pb *PersonBuilder) Build() Person {
   return pb.Person
}

这样就可以更方便的创建Person对象

pb := PersonBuilder{}

person := pb.Create("1", 1).
   WithWeight(60.0).
   WithAddr("aa").
   Build()

可以很明显的看出来,这种建造者模式不需要额外的Config 对象,但是多了一个PersonBuilder,有没有办法直接在Person上进行build呢?也是可以的,这种就是我们要说的函数式编程

Functional Options

首先先定义一个Option函数, 接收一个Person对象指针。

type Option func(*Person)

再定义属性设置的函数,返回一个Option函数

func Weight(w float32) Option {
   return func(person *Person) {
      person.Weight = w
   }
}

func Addr(addr string) Option {
   return func(person *Person) {
      person.Addr = addr
   }
}

上面的代码在传入一个参数后返回一个函数。返回的这个函数会设置自己的 Person 属性值。

例如,当调用 Weight(30.1) 时,返回一个如下函数

func(p *Person) { 
    p.Weight = 30.1 
} 

有点类似JS中的高阶函数。

现在我们就可以定一个 NewPerson()的函数,其中,有一个可变参数 options ,它可以传出多个上面的函数,然后使用一个 for-loop 来设置我们的 Person 对象。

func NewPerson(name string, age int, options ...Option) *Person {
   p := &Person{
      Name: name,
      Age:  age,
   }

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

然后就可以像如下方式创建Person对象

person1 := NewPerson("11", 11)
person2 := NewPerson("22", 22, Weight(22.22))
person3 := NewPerson("33", 33, Addr("33"))
person4 := NewPerson("44", 44, Addr("44"), Weight(44.44))

函数式编程不但解决了使用Config对象方式的时候空判断问题,是放 nil 还是放 Config{}的问题,也不需要额外创建一个 Builder对象。