-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.json
More file actions
1 lines (1 loc) · 32.5 KB
/
Copy pathindex.json
File metadata and controls
1 lines (1 loc) · 32.5 KB
1
[{"categories":["computer-programing"],"content":"摘要 基于接口设计是保证代码通用性和扩展性最重要的也最有效的方法,本文将对 Golang 中的 Interface 进行介绍并给出推荐用法。 ","date":"2021-05-26","objectID":"/posts/golang-interface-usages/:0:0","tags":["golang","software-engineering"],"title":"Golang 中 Interface 的用法","uri":"/posts/golang-interface-usages/"},{"categories":["computer-programing"],"content":"引言 Golang 同很多语言一样,特别是 OOP 语言(例如 Java),都支持接口 Interface,来定义一些对象都具备的行为,即行为契约。Inteface 提供了一种抽象机制,能增加系统的模块性和可维护性,从而可以设计和实现具备通用性和扩展性的代码。 Golang 也支持 OOP 范式,但深受 C 语言的影响,在设计理念和哲学理念上,和其他 OOP 语言是有较大差异的,具备自己的特点,Interface 便是如此。本文将对 Golang 中 Interface 进行介绍和说明,并据此给出推荐用法。 ","date":"2021-05-26","objectID":"/posts/golang-interface-usages/:1:0","tags":["golang","software-engineering"],"title":"Golang 中 Interface 的用法","uri":"/posts/golang-interface-usages/"},{"categories":["computer-programing"],"content":"意义 为什么要使用 Interface ?当我们认识到其意义和作用,明白要解决的问题,才能正确地使用。 做正确的事,正确地做事 在软件设计领域,有一个很重要的架构模式是 Interface-based programing (基于接口编程),组件间的应用程序接口(API)调用可能只通过抽象化接口完成,而没有具体的类。基于这种架构,组件 A 使用组件 B 提供的功能时,只通过定义的抽象化接口,能够隐藏组件 B 的实现细节,从而在升级甚至替换组件 B 的时候,只要新的实现符合了接口定义,就不会影响组件 A 的逻辑性和正确性。 这种可替换性也被称之为 Liskov substitution principle(里氏替换原则): 派生类(子类)对象可以在程序中代替其基类(超类)对象。 通过抽象化的接口 Interface,可以进一步提高模块性,将系统分割成一个个模块,相互之间只通过接口进行交互,从而将变更范围控制在单个模块内,进而提高了系统的可维护性。 注意 「分割模块并通过接口交互」本身并不保证另外两个关键属性,高内聚性和低耦合性 但需要注意的是,基于接口编程并不能避免「接口定义」本身变更引发的问题,例如新增一个方法,所有的实现都需要修改以满足新的接口定义,对此,通常有两种基本方法来处理: 保持已有接口不变,新增接口继承自父接口并新增方法 利用版本机制来对外承诺变更兼容性,例如 semantic versioning 2.0 ","date":"2021-05-26","objectID":"/posts/golang-interface-usages/:2:0","tags":["golang","software-engineering"],"title":"Golang 中 Interface 的用法","uri":"/posts/golang-interface-usages/"},{"categories":["computer-programing"],"content":"特点 不同于 Java 等其他语言,Golang 的 Interface 是隐式实现的,采用了类似 duck-typing 的设计: 当看到一只鸟走起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。 注意 准确地说是 structural-typing 设计,duck-typing 顾名思义更好理解一些,但两者还是有些不同,duck-typing 在运行时检测类型是否匹配,而 structural-typing 通常是在编译时检测。 无需像 Java 等语言一样显式地声明实现了某个接口,只要实现了接口的所有方法,就可以作为该接口进行使用: // Java interface public interface Duck { public void walk(); public void quack(); } // 必须显示声明 implements Duck public class Bird implements Duck { public void walk() { // bird walks like a duck } public void quack() { // bird quacks like a duck } } // Golang interface type Duck interface { Walk() Quake() } // 不需要在任何地方显示声明实现了 Duck,只要实现了 Duck 的所有方法即可 type Bird struct {} func (d *Bird) Walk() { // bird walks like a duck } func (d *Bird) Quake() { // bird quacks like a duck } 此外,Golang 有一个特殊的空接口 interface{},可以表示任何类型的变量,因为任何类型都实现了这个空接口,它一般用于处理泛化逻辑,类似 Java 中的 Object: func describe(i interface{}) { fmt.Printf(\"(%v, %T)\\n\", i, i) } 隐式实现比显式实现更简洁,减少了样板代码,同时也保持了松耦合性,尤其是对于实现多个接口的类型,但显式实现也有一些优点: 空接口可以作为标记,再结合反射来实现很强大且方便的功能,例如 Java 的 Serializable 接口。Java 的 Annotation 系统也可以做到,甚至更合适,例如 Spring 的 IOC 容器 。但可惜 Golang 这两种都不支持。 隐式实现只能在使用的时候才能检测\u0008出是否实现了指定接口,显式实现则可以尽早发现,在定义的时候就进行检测。不过 Golang 可以用下面的技巧达到尽早发现的目的: type I interface { Hello() } type A struct {} func (a *A) Hello() {} var _ I = (*A)(nil) // A 如果没有实现 I 则会编译报错 显式实现多接口的时候能够进行冲突检测,例如多个接口有同名方法,C# 甚至可以做到作为不同接口调用时有不同行为。基于简单的哲学理念以及隐式实现的机制,Golang 并不支持这类特性。 ","date":"2021-05-26","objectID":"/posts/golang-interface-usages/:3:0","tags":["golang","software-engineering"],"title":"Golang 中 Interface 的用法","uri":"/posts/golang-interface-usages/"},{"categories":["computer-programing"],"content":"用法 Interface 最主要的作用和用法就是作为中间的抽象层,将服务的 Consumer 和 Provider 进行解耦,从而提高代码的扩展性和可维护性。 在 Java 等显式实现接口的语言中,Provider 处通常会定义 Interface 和实现,Consumer 依赖 Provider 的 Interface,例如: // Provider包 public interface IProvider { public void Foo(); } public class Provider { public void Foo() { // } } // Consumer包 public class Consumer { private IProvider p; public void Bar() { p.Foo(); } } 但在 Golang 中,由于上面介绍的特点,最佳实践却不太一样。在 Golang 社区的 CodeReviewComments 上对此有一些介绍,最关键的一点是 「在 Consumer 处定义 Interface」: Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. package consumer // consumer.go type Thinger interface { Thing() bool } func Foo(t Thinger) string { ... } package producer type Thinger struct{ ... } func (t Thinger) Thing() bool { ... } // 直接返回具体类型,而不是接口类型 func NewThinger() Thinger { return Thinger{ ... } } 为什么要用这种模式呢?主要有以下理由: 对于隐式实现而言,在 Provider 处定义接口已无必要;而显式实现必须要指定接口,或包内或包外 易于扩展,新增方法不会影响已实现的接口,无需修改接口代码 避免循环依赖,在 Golang 中即使只引用了 Interface 也会构成包依赖关系 更符合 Interface segregation principle(接口隔离原则),接口只需要定义必要的方法 当提供满足相同行为契约的不同实现时,在 Provider 处定义接口也是合理的,但需要注意的是,一旦行为契约发生变更,那所有的实现都需要进行修改和适配: type Data struct { ... } type interface DataStore { GetByID(id uint64) Data } type DBDataStore struct { ... } func (s *DBDataStore) GetByID(id uint64) Data { ... } type RedisDataStore struct { ... } func (s *RedisDataStore) GetByID(id uint64) Data { ... } 有的 Provider 提供的服务使用的比较广泛,如果在每个 Consumer 包都定义一样的接口,会显得比较繁琐。对此有一个做法是在另外单独的包内定义通用的接口,但个人不太推荐,主要有以下考虑: Consumer 依赖的接口也是其重要组成部分,定义在包内能保证自完整性,例如 Golang 源码的 sort.Interface 接口 每个 Consumer 单独定义接口可以取更具语义化的名称,从而提高代码的可读性 使用同一个接口也会导致原本可能不相关的模块耦合起来,例如其中一个 Consumer 依赖了新方法,其他 Consumer 也会被迫依赖这个不需要的方法,从而违背了接口隔离原则,长期迭代下去可能会变得相当臃肿 接口定义比较轻量,即使在每个 Consumer 处定义应该也是能接受的 ","date":"2021-05-26","objectID":"/posts/golang-interface-usages/:4:0","tags":["golang","software-engineering"],"title":"Golang 中 Interface 的用法","uri":"/posts/golang-interface-usages/"},{"categories":["computer-programing"],"content":"参考资料 Wikipedia - 面向对象程序设计 Wikipedia - Interface-based programing Wikepedia - Liskov substitution principle semantic versioning 2.0 Golang - Effective Go Wikipedia - Duck Typing Wikipedia - Structural type system Golang - The empty interface Microsoft Docs - 显式接口实现(C# 编程指南) Golang - CodeReviewComments Wikipedia - Interface segregation principle ","date":"2021-05-26","objectID":"/posts/golang-interface-usages/:5:0","tags":["golang","software-engineering"],"title":"Golang 中 Interface 的用法","uri":"/posts/golang-interface-usages/"},{"categories":["computer-programing"],"content":"摘要 在 Golang 语言开发中,对于复杂的、可高度定制的功能,需要有良好的扩展性和兼容性,这里提供一种基于 Option 的设计模式,以解决此类问题。 ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:0:0","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"引言 在程序设计中,优秀的程序员不仅仅要完成需求,为了应对未来的需求变更和功能升级,还需要考虑抽象性和扩展性,并在后续迭代的时候,保证历史功能的兼容性。 本文以常见的 HttpClient 为例,基于 Option 实现在 Golang 语言中的一种通用的、具备高扩展性且易兼容的设计模式。 ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:1:0","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"需求 HttpClient 对于几乎所有程序员而言都不陌生,在使用的时候需要指定很多参数,最基本的包括 Method、URI 等,发送请求后得到 HttpResponse。 显然 HttpClient 就是一种复杂的、可以高度定制的一种功能,假设我们有以下参数配置: Method:只能为 Get 或 Post URI:调用地址 ConnTimeout:链接超时时间,可选项,默认 10 秒 IoTimeout:读写超时时间,可选项,默认 30 秒 Retries:重试次数,可选项,默认 1 次 Pool Size: 连接池大小,可选项,默认 20 个连接 对于上述需求,我们怎么设计,可以让实现的功能,具备更好的扩展性和兼容性呢?接下来由简入繁,介绍几种不同的方案。 ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:2:0","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"基于入参的方案 最直接的方案就是将所有的参数作为方法入参,Pool Size 在构建 HttpClient 的时候传入,而其余参数在 Send 的时候传入: type HttpClient struct { poolSize uint64 } func NewHttpClient(poolSize uint64) *HttpClient { // ...... } func (c *HttpClient) Send(ctx context.Context, method string, uri string, connTimeout, ioTimeout time.Duration, retries uint64) (HttpResponse, error) { // ...... } 很显然,这种方案并不好,存在以下问题: 方法参数过多 可选项参数也必须指定 新增参数需要修改接口 ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:3:0","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"基于入参聚合的方案 对于上一种方案的问题,有一种优化方法,就是参数聚合,将 NewHttpClient 和 Send 方法的入参分别聚合为结构体 HttpClientParams 和 HttpRequest: type HttpClient struct { poolSize uint64 } type HttpClientParams struct { PoolSize uint64 } func NewHttpClient(params HttpClientParams) *HttpClient { // ...... } type HttpRequest struct { Method string URI string ConnTimeout time.Duration IoTimeout time.Duration Retries uint64 } func (c *HttpClient) Send(ctx context.Context, req HttpRequest) (HttpResponse, error) { // ...... } 这种方案通过参数聚合,看似解决了上一种方案的问题: 方法参数过多:只需要传入一个结构体参数 可选项参数也必须指定:构建结构体的时候可以忽略可选项 新增参数需要修改接口:在结构体中加参数不会导致已有代码编译失败 这是基于 Golang 中的 Zero Value 来解决上述问题的,当发现参数值为 Zero Value 的时候,就取可选项的默认值或执行兼容逻辑,但还是存在其他问题: 如果参数的 Zero Value 也是合法值,则无法与默认值进行区分,例如 Retries 无法取 0 以禁用重试 如果想要给 HttpClient 配置参数自定义默认值,例如 Retries,则需要在 HttpClientParams 参数中也加上 Retries 参数,并加上参数校验等逻辑,但对于这种重复代码,优秀的程序员应该尽量避免 ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:4:0","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"基于 Option 的方案 Option 意为选项,所有的选项则构成了 Options,类似游戏中的选项设置。HttpClient 的每个参数分别对应一个 Option,所有的参数加起来,则构成了 HttpClient 的 Options: func Options struct { Method string URI string ConnTimeout time.Duration IoTimeout time.Duration Retries uint64 PoolSize uint64 } 接下来就是这个设计模式的精髓,基于 Function 定义 Option: type Option func(opts *Options) func WithMethod(method string) Option { return func(opts *Options) { opts.Method = method } } func WithRetries(retries uint64) Option { return func(opts *Options) { opts.Retries = retries } } // 其他 WithXXX 方法 ...... Option 是用于修改 Options 的函数,WithXXX 方法可以得到用来指定参数的 Option,接下来修改 HttpClient 相关的方法: type HttpClient struct { opts *Options } func NewHttpClient(opts ...Option) *HttpClient { return HttpClient{opts: newOptions().apply(opts...)} } func (c *HttpClient) Send(ctx context.Context, opts ...Option) (HttpResponse, error) { options := c.opts.clone().apply(opts...) // 基于 options 进行处理,得到 HttpResponse } func newOptions() *Options { return \u0026Options { // 设置选项默认值 } } func (o *Options) apply(opts ...Option) *Options { for _, opt := range opts { opt(o) } return o } func (o *Options) clone() *Options { cloned := *o return \u0026cloned } 如此一来,构建 HttpClient 或者调用 Send 发送请求的时候只需要传入必须的参数即可,即使后面增加再多的参数也能具备很好的兼容性,同时也解决了上一个方案的问题: func Example() { c := NewHttpClient(WithRetries(3)) resp, err := c.Send(context.Background(), WithMethod(\"GET\"), WithRetries(0)) } ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:5:0","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"优化基于 Option 的方案 上面的方案已经是最优的了吗?并不是!基于 Option,可以很容易地进行扩展,接下来会给出几种进一步优化的方法。 ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:6:0","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"与 Builder 设计模式结合 在上面的方案中,Send 方法传入一堆 Option 其实是很奇怪的,因为有的参数并不是可选项,例如 URI,明确区分必传参数和可选参数能让接口定义更为清晰,一种简单的方法就是将必传参数单独列为方法的入参,而其他参数则作为 Option 传入: func (c *HttpClient) Send(ctx context.Context, method string, uri string, opts ...Option) (HttpResponse, error) { // ...... } 但是对于复杂的场景,将 Option 设计模式与 Builder 设计模式结合,能够提供更清晰更易扩展的接口定义: type HttpRequest struct { // 可以获取配置好的选项,如果 HttpClient 也配置了一些选项,则两者合并才是最终结果 options *Options opts []Option valid bool } func (c *HttpClient) Send(ctx context.Context, req HttpRequest) (HttpResponse, error) { options := c.opts.clone().apply(req.opts...) // 基于 options 进行处理,得到 HttpResponse } type HttpRequestBuilder struct { opts []Option } func NewHttpRequestBuilder() *HttpRequestBuilder { return \u0026HttpRequestBuilder{} } func (b *HttpRequestBuilder) Option(o Option) *HttpRequestBuilder { b.opts = append(b.opts, o) return b } // 在 Build 方法指定必传参数 func (b *HttpRequestBuilder) Build(uri string) HttpRequest { opts := append(b.opts, withURI(uri)) options := newOptions().apply(opts...) return HttpRequest { options: options, opts: opts, valid: true, } } func withURI(uri string ) Option { return func(opts *Options) { opts.URI = uri } } ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:6:1","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"选项校验 很多选项需要校验合法性,例如需求中要求 Method 只能为 GET 或 POST,我们可以将 Option 改造为 Interface,增加 Verify 方法: type Option interface { Apply(opts *Options) Verify() error } // 实现 Option 接口 type option struct { applier func(opts *Options) verifier func() error } func (o *option) Apply(opts *Options) { o.applier(opts) } func (o *option) Verify() error { if o.verifier == nil { return nil } return o.verifier() } func WithMethod(method string) Option { return \u0026option { applier: func(opts *Options) { opts.Method = method }, verifier: func() error { if method != \"GET\" \u0026\u0026 method != \"POST\" { return errors.New(\"invalid method\") } return nil }, } } func (o *Options) apply(opts ...Option) (*Options, error) { for _, opt := range opts { if err := opt.Verify(); err != nil { return nil, err } opt.Apply(o) } return o, nil } func NewHttpClient(opts ...Option) (*HttpClient, error) { options, err := newOptions().apply(opts...) if err != nil { return nil, err } return HttpClient{opts: options}, nil } ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:6:2","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"选项组合校验 有时候几个选项相互影响,在更改一个选项的时候,需要判断与其他选项之间的关系,例如如果需要限定 ConnTimeout 必须小于 IoTimeout,上面的校验方式就无法满足了。 一种直观的方式是,对于相互影响的选项,必须同时设置,这样就能进行校验了: func WithTimeouts(connTimeout, ioTimeout time.Duration) Option { return \u0026option { applier: func(opts *options) { opts.connTimeout = connTimeout opts.ioTimeout = ioTimeout }, verifier: func() error { if connTimeout \u003e= ioTimeout { return errors.New(\"invalid connection timeout\") } return nil } } } 要求相关的选项必须同时设置,对于使用者较为繁琐,当只想要更新其中一个选项时,也不得不设置其他选项。但是对于相互影响的选项,使用者如果了解得不够深入,可能会设置不恰当的选项,这种方式反而显示地提醒了使用者,有一定的正面作用。 如果还是希望每个选项能够单独设置,但又能和其他选项一起校验,同时不希望违背单一原则,可以参照下面的模式: type Options struct { TimeoutOptions Method string URI string Retries uint64 PoolSize uint64 } // 相关的选项放在一起,类似游戏设置中的选项组 type TimeoutOptions struct { ConnTimeout time.Duration IoTimeout time.Duration } func (o TimeoutOptions) Verify() error { if o.ConnTimeout \u003e= o.IoTimeout { return errors.New(\"invalid timeout\") } return nil } type Option interface { Apply(opts *Options) Verify(opts *Options) error // 需要加上 Options 入参 } type option struct { applier func(opts *Options) verifier func(opts *Options) error } func (o *option) Verify(opts *Options) error { if o.verifier == nil { return nil } return o.verifier(opts) } func (o *Options) apply(opts ...Option) (*Options, error) { for _, opt := range opts { opt.Apply(o) } // 校验需要后置,因为可能需要组合校验 for _, opt := range opts { if err := opt.Verify(o); err != nil { return nil, err } } return o, nil } func WithConnTimeout(timeout time.Duration) Option { return \u0026option { applier: func(opts *Options) { opts.ConnTimeout = timeout }, verifier: func(opts *Options) error { return opts.TimeoutOptions.Verify() } } } func WithIoTimeout(timeout time.Duration) Option { return \u0026option { applier: func(opts *Options) { opts.IoTimeout = timeout }, verifier: func(opts *Options) error { return opts.TimeoutOptions.Verify() } } } // 有的选项也可以采用同时设置的方式来显示提醒 func WithTimeouts(connTimeout, ioTimeout timeDuration) Option { return \u0026option { applier: func(opts *Options) { opts.ConnTimeout = connTimeout opts.IoTimeout = ioTimeout }, verifier: func(opts *Options) error { return opts.TimeoutOptions.Verify() } } } ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:6:3","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"选项私有化 类似游戏中的选项设置,所有的选项都应该是系统定义的,但现有的 Option 的定义,无论是基于 Function 还是 Inteface 实现的,都可以在包外进行自定义。因此虽然在上面为 Option 增加了校验功能,但使用方可以自己实现接口,从而绕过校验逻辑。 基于 Golang 小写标识符包内隐藏的机制,可以作如下修改,将选项私有化,只能在包内进行定义: // 基于 Function,必须将 Options 改为小写 type Option func(opts *optons) // 基于 Interface,Options 可以不改为小写 type Option interface { apply(opts *options) verify(opts *options) error } type options struct { timeoutOptions method string uri string retries uint64 poolSize uint64 } type timeoutOptions struct { connTimeout time.Duration ioTimeout time.Duration } // 如果 Options 改为了小写,则可以利用 Golang embedded struct 机制,但又不会暴露到外部导致可能被修改 type HttpClient struct { *options } 技巧 将 Option 或者 Options 任何一个改为小写都可以达到私有化的目的,但是修改 Options 更为合理: Options 作为内部选项集合,不应该暴露到包外,改为小写后还可以 embed 到其他 struct Option 是提供给调用方使用的,保持大写暴露到包外可以允许在包外定义预定义选项 ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:6:4","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"选项聚合 在有众多选项的情况下,每次使用都需要传入大量的选项是很繁琐的,可以提供选项聚合的功能,提前定义选项组: // 基于 Function 的 Option func CombineOptions(opts ...Option) Option { return func(o *options) { for _, opt := range opts { opt(o) } } } // 基于 Interface 的 Option func CombineOptions(opts ...Option) Option { return optionGroup(opts) type optionGroup []Option func (g optionGroup) apply(opts *options) { for _, opt := range g { opt.apply(opts) } } func (g optionGroup) verify(opts *options) error { for _, opt := range g { if err := opt.verify(opts); err != nil { return err } } return nil } ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:6:5","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"选项回滚 有时候可能需要临时修改 HttpClient 的 Option,进行一些处理,然后再回滚到之前的 Option。对于这类需求,可以基于 Self-Reference 机制,对 Option 进行改造: // 基于 Function type Option func (opts *options) Option func (c *HttpClient) Option(opt Option) Option { return opt(c.options) } func WithRetries(retries uint64) Option { return func(opts *options) Option { old := opts.retries opts.retries = retries return WithRetries(old) } } // 基于 Interface type Option interface { apply(opts *options) Option verify(opts *options) error } func (c *HttpClient) Option(opt Option) (Option, error) { opts := c.options.clone() o := opt.apply(opts) if err := opt.verify(opts); err != nil { return nil, err } c.options = opts return o, nil } func WithRetries(retries uint64) Option { return \u0026option { applier: func(opts *options) Option { old := opts.retries opts.retires = retries return WithRetries(old) }, } } 但是这种方式并不常用,也不建议,因为会有并发问题,更安全的方式是采用 Immutable 设计,每次变更都创建一个新对象,用完废弃: func (c *HttpClient) WithOption(opt Option) (*HttpClient, error) { // 构建一个新的 HttpClient,带有新的指定选项,但可以复用部分底层设施,例如连接池 } ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:6:6","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"区分 Scope 以 HttpClient 为例,并不是所有 Option 都同时适用 HttpClient 和 HttpRequest,例如 Pool Size 只适用于 HttpClient,如果配置到 HttpRequest 则不起作用也无意义,因此需要区分不同的 Scope: // 仅对 HttpClient 适用 type ClientOption interface { Option client() // 空实现即可 } type clientOption struct { *option } func (o clientOption) client() {} // 仅对 HttpRequest 适用 type RequestOption interface { Option request() // 空实现即可 } type requestOption struct { *option } func (o requestOption) request() {} // 对所有 Scope 适用 type CommonOption interface { ClientOption RequestOption } type commonOption struct { *option } func (o commonOption) client() {} func (o commonOption) request() {} func WithPoolSize(size uint64) ClientOption { return clientOption{\u0026option{ applier: func(opts *options) { opts.poolSize = size }, verifier: func(opts *options) error { if size == 0 { return errors.new(\"invalid pool size\") } return nil }, }} } func WithRetries(retries uint64) CommonOption { return commonOption{\u0026option{ applier: func(opts *options) { opts.retries = retries }, }} } func NewHttpClient(opts ...ClientOption) (*HttpClient, error) { // ...... } 注意 因为 Golang 不支持范型,如果要同时支持选项回滚,将会变得很繁琐,这也是为什么更推荐 Immutable 设计来代替选项回滚的另一个原因。 ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:6:7","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"常用 Option 模式示例 从上面的方案可以看出,基于 Option 可以很容易地进行扩展,但是扩展得越多,也就越复杂。下面给出一个比较常用的组合模式的代码示例,基于以下功能扩展: 选项校验 选项组合校验 选项私有化 选项聚合 // 选项私有化 type options struct { timeoutOptions method string uri string retries uint64 poolSize uint64 } type timeoutOptions struct { connTimeout time.Duration ioTimeout time.Duration } func (o timeoutOptions) verify() error { if o.connTimeout \u003e= o.ioTimeout { return errors.New(\"invalid timeout\") } return nil } type Option interface { apply(opts *options) // 选项私有化 verify(opts *options) error // 选项校验 } // 实现 Option 接口 type option struct { applier func(opts *options) verifier func(opts *options) error } func (o *option) apply(opts *options) { o.applier(opts) } func (o *option) verify(opts *options) error { if o.verifier == nil { return nil } return o.verifier(opts) } func CombineOptions(opts ...Option) Option { return optionGroup(opts) } // 选项聚合 type optionGroup []Option func (g optionGroup) apply(opts *options) { for _, opt := range g { opt.apply(opts) } } func (g optionGroup) verify(opts *options) error { for _, opt := range g { if err := opt.verify(opts); err != nil { return err } } return nil } func WithMethod(method string) Option { return \u0026option{ applier: func(opts *options) { opts.method = method }, verifier: func(opts *options) error { switch method { case \"GET\", \"POST\": return nil default: return errors.New(\"invalid method\") } }, } } func WithRetries(retries uint64) Option { return \u0026option{ applier: func(opts *options) { opts.retries = retries }, } } func WithConnTimeout(timeout time.Duration) Option { return \u0026option { applier: func(opts *options) { opts.connTimeout = timeout }, verifier: func(opts *options) error { return opts.timeoutOptions.verify() }, } } func WithIoTimeout(timeout time.Duration) Option { return \u0026option { applier: func(opts *options) { opts.ioTimeout = timeout }, verifier: func(opts *options) error { return opts.timeoutOptions.verify() }, } } func newOptions() *options { return \u0026options { // 设置选项默认值 } } func (o *options) apply(opts ...Option) (*options, error) { for _, opt := range opts { opt.apply(o) } for _, opt := range opts { if err := opt.verify(o); err != nil { return nil, err } } return o, nil } type HttpClient struct { *options } func NewHttpClient(opts ...Option) (*HttpClient, error) { options, err := newOptions().apply(opts...) if err != nil { return nil, err } return \u0026HttpClient{options: options}, nil } ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:7:0","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"},{"categories":["computer-programing"],"content":"总结 在程序设计领域中,有一些设计原则对于如何开发一个容易进行维护和扩展的系统有着非常重要的指导作用,而其中 S.O.L.I.D 便是其中的五个基本原则,包括: 单一功能原则:Single-responsibility principle 开闭原则:Open-closed principle 里氏替换原则:Liskov substitution principle 接口隔离原则:Interface segregation principle 依赖反转原则:Dependency inversion principle 此外还有一个很重要也是很基本的原则是: DRY:Don’t Repeat Yourself,不要含有任何重复代码 Option 设计模式主要用于解决扩展性的问题,在本文的演变过程中,就很好地体现了这几个原则。 但是需要注意的是,抽象性和扩展性越好的系统往往也越复杂,过度设计反而会给系统带来不必要的复杂性,需要根据实际需求来进行设计。对此,也有一个非常重要的经验原则: KISS 原则:Keep It Simple and Stupid,在设计当中应当注重简约 ","date":"2021-04-15","objectID":"/posts/golang-option-design-pattern/:8:0","tags":["golang","design-pattern","software-engineering"],"title":"Golang 中的 Option 设计模式","uri":"/posts/golang-option-design-pattern/"}]