建造者模式
工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。
建造者模式是用来创建一种类型的复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
其实在 Golang
中对于创建类参数比较多的对象的时候,我们常见的做法是必填参数直接传递,可选参数通过传递可变的方法进行创建
。
builder 模式
package main
import (
"errors"
"fmt"
"time"
)
type Options struct {
ConnTimeout time.Duration
ReadTimeout time.Duration
RetryTime int32
}
// 传统模式 一
// 存在问题:参数序列长,不利于维护
// 改进:如果将传入参数改为一个参数对象,可以解决参数序列过长的问题,但参数对象可能也会很庞大,进而出现其他问题
func NewOptionsV1(connTimeout, readTimeout time.Duration, RetryTime int32) (opt Options, err error) {
opt = Options{
ConnTimeout: connTimeout,
ReadTimeout: readTimeout,
RetryTime: RetryTime,
}
if opt.ReadTimeout < opt.ConnTimeout {
err = errors.New("error params")
return
}
return
}
// 传统模式 二
// 存在问题:校验逻辑(必填参数、依赖关系校验)放在什么地方
func NewOptionsV2() *Options {
return &Options{}
}
func (opt *Options) SetConnTimeout(connTimeout time.Duration) {
opt.ConnTimeout = connTimeout
}
func (opt *Options) SetReadTimeout(readTimeout time.Duration) {
opt.ReadTimeout = readTimeout
}
func (opt *Options) SetRetryTime(retryTime int32) {
opt.RetryTime = retryTime
}
// builder 模式
func (opt *Options) build() (err error) {
// 校验逻辑
if opt.ReadTimeout < opt.ConnTimeout {
err = errors.New("error params")
return
}
return
}
func (opt *Options) setConnTimeout(timeout time.Duration) *Options {
opt.ConnTimeout = timeout
return opt
}
func (opt *Options) setReadTimeout(timeout time.Duration) *Options {
opt.ReadTimeout = timeout
return opt
}
func (opt *Options) setRetryTime(times int32) *Options {
opt.RetryTime = times
return opt
}
func Build() {
opt := &Options{}
err := opt.
setConnTimeout(100 * time.Second).
setReadTimeout(1000 * time.Second).
setRetryTime(1).
build()
if err != nil {
panic(err)
}
fmt.Printf("BuildV1: %+v\n", opt)
}
函数式选项模式:Go 当中用得更多
// 变体:Go 当中用得更多
type Opt func(option *Options)
func WithConnTimeout(connTimeout time.Duration) Opt {
return func(option *Options) {
option.ConnTimeout = connTimeout
}
}
func WithRetryTime(retryTime int32) Opt {
return func(option *Options) {
option.RetryTime = retryTime
}
}
func NewOptionsV3(requiredParams1 int, requiredParams2 string, opts ...Opt) *Options {
// 里面可有默认参数
options := &Options{}
// 其余参数通过传进来的一个接受目标对象的函数列表来一一设置目标对象的相应属性
for _, opt := range opts {
opt(options)
}
// 校验逻辑
return options
}
func BuildV2() {
opt := NewOptionsV3(1, "test", WithConnTimeout(1*time.Second), WithRetryTime(2))
fmt.Printf("BuildV2: %+v\n", opt)
}
再举一列
package builder
import "fmt"
// ResourcePoolConfigOption option
type ResourcePoolConfigOption struct {
maxTotal int
maxIdle int
minIdle int
}
// ResourcePoolConfigOptFunc to set option
type ResourcePoolConfigOptFunc func(option *ResourcePoolConfigOption)
// NewResourcePoolConfig NewResourcePoolConfig
func NewResourcePoolConfig(name string, opts ...ResourcePoolConfigOptFunc) (*ResourcePoolConfig, error) {
if name == "" {
return nil, fmt.Errorf("name can not be empty")
}
// 目标对象可有默认值
option := &ResourcePoolConfigOption{
maxTotal: 10,
maxIdle: 9,
minIdle: 1,
}
// 可选参数的设置
for _, opt := range opts {
opt(option)
}
// 校验逻辑
if option.maxTotal < 0 || option.maxIdle < 0 || option.minIdle < 0 {
return nil, fmt.Errorf("args err, option: %v", option)
}
if option.maxTotal < option.maxIdle || option.minIdle > option.maxIdle {
return nil, fmt.Errorf("args err, option: %v", option)
}
return &ResourcePoolConfig{
name: name,
maxTotal: option.maxTotal,
maxIdle: option.maxIdle,
minIdle: option.minIdle,
}, nil
}
应用场景
- 我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填属性通过
set()
方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。 - 如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合
set()
方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。 - 如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露
set()
方法。构造函数配合set()
方法来设置属性值的方式就不适用了。
总结
其实可以看到,绝大多数情况下直接使用后面的这种方式就可以了,并且在编写公共库的时候,强烈建议入口的参数都可以这么传递,这样可以最大程度的保证我们公共库的兼容性,避免在后续的更新的时候出现破坏性的更新的情况。
实际工作中,db
操作go
语言基本都选择的gorm
,此时就可以灵活使用函数式选项模式拼接db
很多where
条件