[设计模式 in Golang]选项模式

本是古典 何须时尚 2022-09-11 10:24 246阅读 0赞

前言

在很多优秀的Go语言项目中我们都可以见到选项模式(Options Pattern),例如grpc/grpc-go的NewServer函数、uber-go/zap 包的New函数都用到了选项模式。

使用选项模式,我们可以非常华丽地实现——带许多默认值且可选地修改其中某些值的工厂方法或者函数。这弥补了Go没有给参数设置默认值的语法的问题。大大简化开发者创建一个对象的成本,尤其是在对象拥有众多属性时。

如何带默认参数地创建对象

多个工厂方法

在创建一个对象的时候,如果想要有一些默认值,最朴素地,我们可能会为一个对象准备多个工厂方法:

  1. package client
  2. import "time"
  3. const (
  4. DefaultTimeout = 1 * time.Second
  5. )
  6. type DemoClient struct {
  7. host string
  8. timeout time.Duration
  9. }
  10. func NewDemoClient(host string) *DemoClient{
  11. return &DemoClient{
  12. host: host,
  13. timeout: DefaultTimeout,
  14. }
  15. }
  16. func NewDemoClientWithTimeout(host string, timeout time.Duration) *DemoClient{
  17. return &DemoClient{
  18. host: host,
  19. timeout: timeout,
  20. }
  21. }

这种方式十分直观,但是扩展性实在太差,如果多几个参数,比如加个maxretry参数

  1. package client
  2. import "time"
  3. const (
  4. DefaultTimeout = 1 * time.Second
  5. DefaultMaxRetry = 3
  6. )
  7. type DemoClient struct {
  8. host string
  9. timeout time.Duration
  10. maxRetry int
  11. }

那难道我再加:

  1. NewDemoClientWithMaxRetry()
  2. NewDemoClientWithTimeoutAndMaxRetry()

多几个参数还不写疯了。要写排列组合个工厂方法。omg。太不geek了。

提供默认Option参数的工厂

优雅很多地,我们可以让工厂方法接受一个Option结构体,并提供默认的Option。

  1. package client
  2. import "time"
  3. const (
  4. DefaultTimeout = 1 * time.Second
  5. DefaultMaxRetry = 3
  6. )
  7. type DemoClient struct {
  8. host string
  9. timeout time.Duration
  10. maxRetry int
  11. }
  12. type Options struct {
  13. Timeout time.Duration
  14. MaxRetry int
  15. }
  16. func NewDefaultOptions() Options {
  17. return Options{
  18. Timeout: DefaultTimeout,
  19. MaxRetry: DefaultMaxRetry,
  20. }
  21. }
  22. func NewDemoClient(host string, opt Options) *DemoClient {
  23. return &DemoClient{
  24. host: host,
  25. timeout: opt.Timeout,
  26. maxRetry: opt.MaxRetry,
  27. }
  28. }

这样构造一个Client的代码就长成这样了:

  1. opt := NewDefaultOptions()
  2. opt.Timeout = 3 * time.Second
  3. client := NewDemoClient("127.0.0.1:8888", opt)

或者也可以不提供DefaultOption,0值如果无意义直接赋值为默认值,如果0值有意义,Option中就改成指针:

  1. package client
  2. import "time"
  3. const (
  4. DefaultTimeout = 1 * time.Second
  5. DefaultMaxRetry = 3
  6. )
  7. type DemoClient struct {
  8. host string
  9. timeout time.Duration
  10. maxRetry int
  11. }
  12. type Options struct {
  13. Timeout time.Duration
  14. MaxRetry *int
  15. }
  16. func NewDemoClient(host string, opt Options) *DemoClient {
  17. client := &DemoClient{
  18. host: host,
  19. timeout: DefaultTimeout,
  20. maxRetry: DefaultMaxRetry,
  21. }
  22. if opt.Timeout != 0 {
  23. client.timeout = opt.Timeout
  24. }
  25. if opt.MaxRetry != nil {
  26. client.maxRetry = *opt.MaxRetry
  27. }
  28. return client
  29. }

这里把Options.MaxRetry从int改成了*int是应为0值是有意义的,代表不重试。为了能让默认是有重试的,得要让用户显式地指定重试次数,赋值一个指针。

这样,很简单的就能创建一个默认的client。

  1. client := NewDemoClient("127.0.0.1:8888", Options{
  2. })

这种方法其实已经蛮简单,扩展性很好,满足大部分需求了。如果需要加其他有默认值的参数,只需要往Options里加就行,还能兼容以前的代码。唯一一点小缺点就是还是要创建Options,还是麻烦了一点。

如果想要更加geek一点,那我们可以整上选项模式

选项模式

以下代码通过选项模式实现上述功能:

  1. package client
  2. import (
  3. "time"
  4. )
  5. const (
  6. DefaultTimeout = 1 * time.Second
  7. DefaultMaxRetry = 3
  8. )
  9. type DemoClient struct {
  10. host string
  11. timeout time.Duration
  12. maxRetry int
  13. }
  14. type Option func(opt *Options)
  15. type Options struct {
  16. Timeout time.Duration
  17. MaxRetry int
  18. }
  19. var defaultOption = Options{
  20. Timeout: DefaultTimeout,
  21. MaxRetry: DefaultMaxRetry,
  22. }
  23. func WithTimeout(timeout time.Duration) Option {
  24. return func(opt *Options) {
  25. opt.Timeout = timeout
  26. }
  27. }
  28. func WithMaxRetry(maxRetry int) Option {
  29. return func(opt *Options) {
  30. opt.MaxRetry = maxRetry
  31. }
  32. }
  33. func NewDemoClient(host string, opts ...Option) *DemoClient {
  34. opt := defaultOption
  35. for _, o := range opts {
  36. o(&opt)
  37. }
  38. return &DemoClient{
  39. host: host,
  40. timeout: opt.Timeout,
  41. maxRetry: opt.MaxRetry,
  42. }
  43. }

在上面的代码中,我们定义了Option函数类型,然后利用Go语言的闭包特性,提供工厂方法来返回按需修改Options各字段的Option函数。在 NewDemoClient中,先用defaultOption初始化默认的Options,然后通过回调

  1. for _, o := range opts {
  2. o(&opt)
  3. }

修改Options内的字段。最后创建出对象。

这里通过利用可变参数列表的特性,同样是构建默认对象,只需要:

  1. client := NewDemoClient("127.0.0.1:8888")

比之前的做法少了个Option{}。

带参数时:

  1. client := NewDemoClient("127.0.0.1:8888", WithMaxRetry(3), WithTimeout(3 * time.Second))

并且这种做法使我们实现WithXXXX时能利用闭包的特性实现更大程度的灵活性。比如

  1. // 在around上下浮动5%的超时时间
  2. func WithInaccurateTimeout(around time.Duration) Option {
  3. return func(opt *Options) {
  4. rate := float64(95 + rand.Intn(11)) / 100
  5. opt.Timeout = time.Duration(float64(around) * rate)
  6. }
  7. }

在这个示例中,其实可以直接往Option里传Client:

  1. package client
  2. import (
  3. "time"
  4. )
  5. const (
  6. DefaultTimeout = 1 * time.Second
  7. DefaultMaxRetry = 3
  8. )
  9. type DemoClient struct {
  10. host string
  11. timeout time.Duration
  12. maxRetry int
  13. }
  14. type Option func(opt *DemoClient)
  15. func WithTimeout(timeout time.Duration) Option {
  16. return func(opt *DemoClient) {
  17. opt.timeout = timeout
  18. }
  19. }
  20. func WithMaxRetry(maxRetry int) Option {
  21. return func(opt *DemoClient) {
  22. opt.maxRetry = maxRetry
  23. }
  24. }
  25. func NewDemoClient(host string, opts ...Option) *DemoClient {
  26. client := &DemoClient{
  27. host: host,
  28. timeout: DefaultTimeout,
  29. maxRetry: DefaultMaxRetry,
  30. }
  31. for _, o := range opts {
  32. o(client)
  33. }
  34. return client
  35. }

但不是所有时候都可以这么简化,比如有 只在构造时有效,不需要存到对象的字段中的参数 的时候。

结语

选项模式有很多优点,例如:支持传递多个参数,并且在参数发生变化时保持兼容性;支持任意顺序传递参数;支持默认值;方便扩展;通过 WithXXX 的函数命名,可以使参数意义更加明确,等等。

但缺点是,为了实现选项模式,我们显著地增加了很多代码,实际开发中,要根据场景选择是否使用选项模式。

这里只是示例了工厂函数怎么使用选项模式,通常意义上的函数也可以使用,比如 grpc 中的 rpc 方法就是采用选项模式设计的。

选项模式通常适用于以下场景:

  • 结构体参数很多,创建结构体时,我们期望创建一个携带默认值的结构体变量,并选择性修改其中一些参数的值。
  • 结构体参数经常变动,考虑以后的扩展,变动时我们又不想修改创建实例的函数。

如果结构体参数比较少,可以慎重考虑要不要采用选项模式,也许直接用带Option的工厂就足够了。

参考

Functional Options Pattern in Go
golang 设计模式之选项模式

发表评论

表情:
评论列表 (有 0 条评论,246人围观)

还没有评论,来说两句吧...

相关阅读

    相关 [设计模式 in Golang]单例模式

    前言 单例模式是最简单的一个模式,指的是全局只有一个实例,并且它负责创建自己的对象。 单例模式不仅有利于减少内存开支,还有减少系统性能开销、防止多个实例产生冲突等优点。

    相关 golang设计模式(3)组合模式

    组合模式,使我们在树形结构问题中,使用者可以忽略简单元素和复杂元素的概念。客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素内部结构解耦。当应用场景出现分

    相关 golang设计模式(2)工厂模式

    工厂模式根据条件产生不同功能类,工厂模式在解耦方面将使用者和产品之间的依赖推给了工厂,让工厂承担这种依赖关系。工厂模式分简单工厂模式、方法工厂模式和抽象工厂模式。Golang实