探討兩種 option 編程模式的實現

前言

哈嘍,大家好,我是asongoption編程模式大家一定熟知,但是其寫法不唯一,主要是形成了兩個版本的option設計,本文就探討一下其中的優缺點。

option編程模式的引出

在我們日常開發中,經常在初始化一個對象時需要進行屬性配置,比如我們現在要寫一個本地緩存庫,設計本地緩存結構如下:

type cache struct {
 // hashFunc represents used hash func
 HashFunc HashFunc
 // bucketCount represents the number of segments within a cache instance. value must be a power of two.
 BucketCount uint64
 // bucketMask is bitwise AND applied to the hashVal to find the segment id.
 bucketMask uint64
 // segment is shard
 segments []*segment
 // segment lock
 locks    []sync.RWMutex
 // close cache
 close chan struct{}
}

在這個對象中,字段hashFuncBucketCount是對外暴露的,但是都不是必填的,可以有默認值,針對這樣的配置,因爲Go語言不支持重載函數,我們就需要多種不同的創建不同配置的緩存對象的方法:

func NewDefaultCache() (*cache,error){}
func NewCache(hashFunc HashFunc, count uint64) (*cache,error) {}
func NewCacheWithHashFunc(hashFunc HashFunc) (*cache,error) {}
func NewCacheWithBucketCount(count uint64) (*cache,error) {}

這種方式就要我們提供多種創建方式,以後如果我們要添加配置,就要不斷新增創建方法以及在當前方法中添加參數,也會導致NewCache方法會越來越長,爲了解決這個問題,我們就可以使用配置對象方案:

type Config struct {
  HashFunc HashFunc
  BucketCount uint64
}

我們把非必填的選項移動config結構體內,創建緩存的對象的方法就可以只提供一個,變成這樣:

func DefaultConfig() *Config {}
func NewCache(config *Config) (*cache,error) {}

這樣雖然可以解決上述的問題,但是也會造成我們在NewCache方法內做更多的判空操作,config並不是一個必須項,隨着參數增多,NewCache的邏輯代碼也會越來越長,這就引出了option編程模式,接下來我們就看一下option編程模式的兩種實現。

option 編程模式一

使用閉包的方式實現,具體實現:

type Opt func(options *cache)

func NewCache(opts ...Opt) {
 c := &cache{
  close: make(chan struct{}),
 }
 for _, each := range opts {
  each(c)
 }
}

func NewCache(opts ...Opt) (*cache,error){
 c := &cache{
  hashFunc: NewDefaultHashFunc(),
  bucketCount: defaultBucketCount,
  close: make(chan struct{}),
 }
 for _, each := range opts {
  each(c)
 }
  ......
}

func SetShardCount(count uint64) Opt {
 return func(opt *cache) {
  opt.bucketCount = count
 }
}

func main() {
 NewCache(SetShardCount(256))
}

這裏我們先定義一個類型Opt,這就是我們optionfunc型態,其參數爲*cache,這樣創建緩存對象的方法是一個可變參數,可以給多個options,我們在初始化方法裏面先進行默認賦值,然後再通過for loop將每一個options對緩存參數的配置進行替換,這種實現方式就將默認值或零值封裝在NewCache中了,新增參數我們也不需要改邏輯代碼了。但是這種實現方式需要將緩存對象中的field暴露出去,這樣就增加了一些風險,其次client端也需要了解Option的參數是什麼意思,才能知道要怎樣設置值,爲了減少client端的理解度,我們可以自己提前封裝好option函數,例如上面的SetShardCountclient端直接調用並填值就可以了。

option 編程模式二

這種option編程模式是uber推薦的,是在第一版本上面的延伸,將所有options的值進行封裝,並設計一個Option interface,我們先看例子:

type options struct {
 hashFunc HashFunc
 bucketCount uint64
}

type Option interface {
 apply(*options)
}

type Bucket struct {
 count uint64
}

func (b Bucket) apply(opts *options) {
 opts.bucketCount = b.count
}

func WithBucketCount(count uint64) Option {
 return Bucket{
  count: count,
 }
}

type Hash struct {
 hashFunc HashFunc
}

func (h Hash) apply(opts *options)  {
 opts.hashFunc = h.hashFunc
}

func WithHashFunc(hashFunc HashFunc) Option {
 return Hash{hashFunc: hashFunc}
}

func NewCache(opts ...Option) (*cache,error){
 o := &options{
  hashFunc: NewDefaultHashFunc(),
  bucketCount: defaultBucketCount,
 }
 for _, each := range opts {
  each.apply(o)
 }
  .....
}

func main() {
 NewCache(WithBucketCount(128))
}

這種方式我們使用Option接口,該接口保存一個未導出的方法,在未導出的options結構上記錄選項,這種模式爲client端提供了更多的靈活性,針對每一個option可以做更細的custom function設計,更加清晰且不暴露cache的結構,也提高了單元測試的覆蓋性,缺點是當cache結構發生變化時,也要同時維護option的結構,維護複雜性升高了。

總結

這兩種實現方式都很常見,其都有自己的優缺點,採用閉包的實現方式,我們不需要爲維護option,維護者的編碼也大大減少了,但是這種方式需要export對象中的field,是有安全風險的,其次是client端需要了解對象結構中參數的意義,才能寫出option參數,不過這個可以通過自定義option方法來解決;採用接口的實現方式更加靈活,每一個option都可以做精細化設計,不需要export對象中的field,並且很容易進行調試和測試,缺點是需要維護兩套結構,當對象結構發生變更時,option結構也要變更,增加了代碼維護複雜性。

實際應用中,我們可以自由變化,不能直接定義哪一種實現就是好的,凡事都有兩面性,適合纔是最好的。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_7ofuy2Wp__3B9esO_2oag