如何设计一个优雅的类型初始化函数?

161 阅读5分钟

在 Go 语言中,如果要封装一个对象,首先需要定义一个新的类型(结构体),其次是提供一个用于为此类型初始化的函数,代码如下:

type Student struct {
    Name    string
    Age     int
    Sex     bool
    Address string
    Email   string
}

func New(name string, age int, sex bool) *Student {
    return &Student{name, age, sex, "湖南省", "1314@qq.com"}
}

// 调用New函数获取Student实例
stu := New("aitao", 100, true)

此时,初始化函数只为部分字段提供了初始化操作,其它字段都是默认值,那么要为这些可选字段提供初始化操作该如何实现呢?

🚨 需要注意的一点,在 Go 语言中是不支持函数重载机制的,即只要是在同一个包中就不允许存在相同名称的函数!!!

下面是一种比较简单的写法,代码如下:

type Student struct {
    Name    string
    Age     int
    Sex     bool
    // 组合 Options 类型
    Options *Options
}

// 定义可选参数类型
type Options struct {
    Address string
    Email   string
}

func New(name string, age int, sex bool, options *Options) *Student {
    if options != nil {
            if options.Email != "" {
                    options.Email = "湖南省"
            }

            if options.Address != "" {
                    options.Address = "1314@qq.com"
            }
    }
    return &Student{name, age, sex, options}
}

上述代码提供一个Options类型来为可选字段提供初始化功能,在New函数中依次校验可选字段,如果用户未提供了可选字段值,初始化函数会提供默认值。

那么问题来了,如果可选字段有很多,那不是得一个一个校验?怎么解决呢?有两种方法,一种是使用闭包,另一种是使用接口。

闭包实现

采用闭包方式在创建对象时传递可选参数,分为以下几个步骤:

1.明确需要初始化的对象(Student);

type Student struct {
    Name    string
    Age     int
    Sex     bool
    Address string
    Email   string
}

2.定义选项函数类型(Options);

type Options func(*Student)

3.实现具体的选项函数,这里的可选参数只有两个,因此只需要定义两个选项函数;

func WithEmail(email string) Options {
    return func(student *Student) {
            student.Email = email
    }
}

func WithAddress(address string) Options {
    return func(student *Student) {
            student.Address = address
    }
}

4.重写对象初始化函数;

func New(name string, age int, sex bool, options ...Options) *Student {
    sPtr := &Student{name, age, sex, "湖南省", "1314@qq.com"}
    // 遍历可选参数
    for _, option := range options {
            option(sPtr) // 调用对应的闭包函数赋值
    }
    return sPtr
}

5.调用对象初始化函数: 在调用构造函数时,可以传递必需的字段和任意数量的选项接口来定制结构体实例。

// 调用 New 函数获取 Student 实例
stu1 := New("aitao", 100, true)
// 调用选项函数为指定可选参数初始化值
stu2 = New("aitao", 100, true, WithEmail("1314@qq.com"))
stu3 := New("aitao", 100, true, WithEmail("1314@qq.com"), WithAddress("北京市"))

优点

1.灵活性高:闭包方式允许在运行时动态地创建和组合选项函数。这使得选项逻辑可以更加灵活和复杂,适用于需要动态配置的场景。

2.代码简洁: 闭包方式的代码通常比接口方法更简洁。每个选项函数都是一个简单的闭包,不需要定义额外的类型和方法。

3.易组合:闭包方式可以轻松地组合多个选项函数,形成复杂的选项逻辑。例如,可以通过组合多个闭包函数来实现复杂的初始化逻辑。

缺点

1.类型安全差:闭包方式的类型安全性不如接口方法。由于闭包函数是动态创建的,编译器无法在编译时检查每个闭包函数的类型和行为。

2.可读性差: 闭包方式的代码可读性可能不如接口方法。尤其是在选项函数较多或逻辑较复杂的情况下,闭包方式的代码可能会显得较为混乱。

接口实现

采用接口方式在创建对象时传递可选参数,分为以下几个步骤: 1.明确需要初始化的对象(Student); 2.定义选项接口(Options);

type Options interface {
    apply(*Student)
}

3.实现具体选项类型: 需要为每个可选字段实现一个具体的选项类型,并实现 Options 接口的 apply 方法。每个选项类型都是一个自定义类型,通常是对基本类型的封装。

// 针对 Email 字段实现一个选择类型 
type emailOption string

func (eo emailOption) apply(s *Student) {
    s.Email = string(eo)
}

func WithEmail(email string) Options {
    return emailOption(email)
}

// 针对 Address 字段实现一个选择类型 
type addressOption string

func (ao addressOption) apply(s *Student) {
    s.Address = string(ao)
}

func WithAddress(address string) Options {
    return addressOption(address)
}

4.重写对象初始化函数;

func New(name string, age int, sex bool, options ...Options) *Student {
    stu := &Student{name, age, sex, "湖南省", "1314@qq.com"}
    // 遍历可选参数
    for _, option := range options {
            option.apply(stu) // 调用 Options 接口对应方法
    }
    return stu
}

5.调用对象初始化函数: 在调用构造函数时,可以传递必需的字段和任意数量的选项接口来定制结构体实例。

// 调用 New 函数获取 Student 实例
stu1 := New("aitao", 100, true)
// 调用选项函数为指定可选参数初始化值
stu2 = New("aitao", 100, true, WithEmail("1314@qq.com"))
stu3 := New("aitao", 100, true, WithEmail("1314@qq.com"), WithAddress("北京市"))

优点

1.类型安全:使用接口方法实现选项模式时,每个选项类型都必须实现Options接口的apply方法。这确保了每个选项类型都符合预期的行为,提供了更好的类型安全性。

2.扩展性:通过接口,可以轻松地添加新的选项类型,而不需要修改现有的代码。只需定义一个新的类型并实现Options接口即可。

3.代码清晰: 接口方法的实现方式使得代码结构更加清晰,每个选项类型的职责明确,易于理解和维护。

缺点

1.类型转换:每个选项类型都需要定义一个自定义类型,并实现apply方法。这可能会导致代码量增加,尤其是在选项较多的情况下。

2.灵活性差:接口方法的实现方式相对固定,不如闭包方式灵活。例如,闭包方式可以通过组合多个闭包函数来实现复杂的选项逻辑,而接口方法则需要定义更多的类型和方法。