Go使用选项模式的优点是什么?

之前在写一个限流器的时候,习惯性的用了选项模式来配置

type LeakyLimiter struct {
    lock     sync.Mutex
    last     time.Time
    sleep    time.Duration
    perReq   time.Duration
    maxSlack time.Duration
}

type leakyOptions struct {
    maxSlack time.Duration
}

type LeakyOption interface {
    apply(*leakyOptions)
}

type leakyOptionFunc func(options *leakyOptions)

func (l leakyOptionFunc) apply(o *leakyOptions) {
    l(o)
}

func WithMaxSlack(maxSlack time.Duration) LeakyOption {
    return leakyOptionFunc(func(options *leakyOptions) {
        options.maxSlack = maxSlack
    })
}

func NewLeakyLimiter(rate int, opts ...LeakyOption) *LeakyLimiter {
    options := leakyOptions{
        maxSlack: -10 * time.Second / time.Duration(rate),
    }

    for _, option := range opts {
        option.apply(&options)
    }

    return &LeakyLimiter{
        perReq:   time.Second / time.Duration(rate),
        maxSlack: options.maxSlack,
    }
}

但是突然想到,能不能直接暴露出能够修改可配置项的函数

type LeakyLimiter struct {
    lock     sync.Mutex
    last     time.Time
    sleep    time.Duration
    perReq   time.Duration
    maxSlack time.Duration
}

func (l *LeakyLimiter) WithMaxSlack(maxSlack time.Duration) *LeakyLimiter {
    l.maxSlack = maxSlack
    return l
}

func NewLeakyLimiter(rate int) *LeakyLimiter {
    return &LeakyLimiter{
        perReq:   time.Second / time.Duration(rate),
        maxSlack: -10 * time.Second / time.Duration(rate),
    }
}

相比于这种直接配置的方式,选项模式的优点在哪里呢?感觉扩展性和可读性也没有提升,这种方式也可以链式调用。
我个人想的可能是不暴露修改的接口,在调用构造函数以后就不会被修改了安全一点。
除了这一个以外还有其他的考量吗?希望听听各位大大的分析

阅读 1.3k
avatarAI BotBETA

选项模式(或称为策略模式)的主要优点在于它允许用户通过封装和组合不同的行为或策略来定制对象的行为,而不是通过直接修改对象。这在许多情况下都是非常有用的。这里有几个主要的好处:

  1. 易于扩展和维护:如果你有很多不同的行为或配置选项,并且你经常需要添加新的选项,那么使用选项模式会比直接在结构体中暴露这些配置更容易。你可以为每个选项创建一个新的类型,然后将这些类型组合到一个结构体中。这样,你可以在不破坏现有代码的情况下添加新的选项。
  2. 更好的封装:通过将具体实现封装在选项中,而不是直接在结构体中实现,可以更好地保护对象的内部状态。这也有助于实现依赖倒置和更灵活的设计。
  3. 更灵活的配置:选项模式允许你在运行时动态地改变对象的行为。例如,你可以创建一个具有不同行为的 LeakyLimiter 对象用于不同的场景。
  4. 更好的文档和注释:由于选项模式通常要求为每个选项创建单独的类型,这使得代码更加清晰和易于理解。每个选项都有自己的类型,这有助于编写更好的文档和注释。
  5. 链式调用:如你所注意到的,选项模式确实可以支持链式调用。这使得代码更加简洁和易于阅读。当然,不直接在结构体中暴露可修改的函数也可以提供更高的安全性。
  6. 更好的错误处理:如果每个选项都有自己的类型,那么在配置对象时,你就可以更容易地检查选项的有效性,并在必要时提供默认值。

在你的例子中,使用选项模式并没有提供上述所有优点,但确实有助于更好地组织代码,使其更容易扩展和维护。尤其是当你需要为 LeakyLimiter 添加更多配置选项时,使用选项模式会比直接在结构体中添加更多字段更有优势。

3 个回答

个人经验来参考:

  • 因为 Go 的常驻内存,一种是初始化启动、还一种是按需启动。
  • 常驻内存下,多个依赖的服务,其实希望的是 *LeakyLimiter 是一致的,如果改成对象调整,当 A、B 两个依赖这个服务的指针会因为内部变化而影响另外一个。

关于第二点的补充:比如,c2c1 因为调用的是同一个 l,因为 l.WithCount(3) 导致 l 会互相影响

package main

import "fmt"

func main() {
    l := NewLimiter(5)

    c1 := NewCase(l)

    c2 := NewCase(l.WithCount(3))

    fmt.Println(c1, c2)
}

type Case struct {
    limiter *Limiter
}

func NewCase(limiter *Limiter) *Case {
    return &Case{
        limiter: limiter,
    }
}

func (c *Case) Allow() bool {
    return c.limiter.Allow()
}

type Limiter struct {
    Count int
}

func NewLimiter(count int) *Limiter {
    return &Limiter{
        Count: count,
    }
}

func (l *Limiter) WithCount(c int) *Limiter {
    l.Count = c

    return l
}

func (l *Limiter) Allow() bool {
    return l.Count > 0
}

AI回答的很好,你可以仔细看一下。

新手上路,请多包涵

https://go-zing.github.io/gozz/zh/guide/plugins/option.html

Gozz工具可以自动地生成 Functional Options 风格的代码

//go:generate gozz run -p "option" ./

// +zz:option
type Config struct {
    // connect host
    Host string
    // connect port
    Port string
    // database username
    Username string
    // database password
    Password string
}

执行 gozz run -p "option" ./ ,生成了 zzgen.option.go 文件

// Code generated by gozz:option github.com/go-zing/gozz. DO NOT EDIT.

package option01

import ()

// apply functional options for Config
func (o *Config) applyOptions(opts ...func(*Config)) {
    for _, opt := range opts {
        opt(o)
    }
}

// connect host
func WithHost(v string) func(*Config) { return func(o *Config) { o.Host = v } }

// connect port
func WithPort(v string) func(*Config) { return func(o *Config) { o.Port = v } }

// database username
func WithUsername(v string) func(*Config) { return func(o *Config) { o.Username = v } }

// database password
func WithPassword(v string) func(*Config) { return func(o *Config) { o.Password = v } }
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题