探討兩種 Option 編程模式的實現(xiàn)
option編程模式的引出
在我們?nèi)粘i_發(fā)中,經(jīng)常在初始化一個對象時需要進行屬性配置,比如我們現(xiàn)在要寫一個本地緩存庫,設(shè)計本地緩存結(jié)構(gòu)如下:
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{}
}
在這個對象中,字段hashFunc、BucketCount是對外暴露的,但是都不是必填的,可以有默認值,針對這樣的配置,因為Go語言不支持重載函數(shù),我們就需要多種不同的創(chuàng)建不同配置的緩存對象的方法:
func NewDefaultCache() (*cache,error){}
func NewCache(hashFunc HashFunc, count uint64) (*cache,error) {}
func NewCacheWithHashFunc(hashFunc HashFunc) (*cache,error) {}
func NewCacheWithBucketCount(count uint64) (*cache,error) {}
這種方式就要我們提供多種創(chuàng)建方式,以后如果我們要添加配置,就要不斷新增創(chuàng)建方法以及在當(dāng)前方法中添加參數(shù),也會導(dǎo)致NewCache方法會越來越長,為了解決這個問題,我們就可以使用配置對象方案:
type Config struct {
HashFunc HashFunc
BucketCount uint64
}
我們把非必填的選項移動config結(jié)構(gòu)體內(nèi),創(chuàng)建緩存的對象的方法就可以只提供一個,變成這樣:
func DefaultConfig() *Config {}
func NewCache(config *Config) (*cache,error) {}
這樣雖然可以解決上述的問題,但是也會造成我們在NewCache方法內(nèi)做更多的判空操作,config并不是一個必須項,隨著參數(shù)增多,NewCache的邏輯代碼也會越來越長,這就引出了option編程模式,接下來我們就看一下option編程模式的兩種實現(xiàn)。
option編程模式一
使用閉包的方式實現(xiàn),具體實現(xiàn):
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,這就是我們option的func型態(tài),其參數(shù)為*cache,這樣創(chuàng)建緩存對象的方法是一個可變參數(shù),可以給多個options,我們在初始化方法里面先進行默認賦值,然后再通過for loop將每一個options對緩存參數(shù)的配置進行替換,這種實現(xiàn)方式就將默認值或零值封裝在NewCache中了,新增參數(shù)我們也不需要改邏輯代碼了。但是這種實現(xiàn)方式需要將緩存對象中的field暴露出去,這樣就增加了一些風(fēng)險,其次client端也需要了解Option的參數(shù)是什么意思,才能知道要怎樣設(shè)置值,為了減少client端的理解度,我們可以自己提前封裝好option函數(shù),例如上面的SetShardCount,client端直接調(diào)用并填值就可以了。
option編程模式二
這種option編程模式是uber推薦的,是在第一版本上面的延伸,將所有options的值進行封裝,并設(shè)計一個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接口,該接口保存一個未導(dǎo)出的方法,在未導(dǎo)出的options結(jié)構(gòu)上記錄選項,這種模式為client端提供了更多的靈活性,針對每一個option可以做更細的custom function設(shè)計,更加清晰且不暴露cache的結(jié)構(gòu),也提高了單元測試的覆蓋性,缺點是當(dāng)cache結(jié)構(gòu)發(fā)生變化時,也要同時維護option的結(jié)構(gòu),維護復(fù)雜性升高了。
總結(jié)
這兩種實現(xiàn)方式都很常見,其都有自己的優(yōu)缺點,采用閉包的實現(xiàn)方式,我們不需要為維護option,維護者的編碼也大大減少了,但是這種方式需要export對象中的field,是有安全風(fēng)險的,其次是client端需要了解對象結(jié)構(gòu)中參數(shù)的意義,才能寫出option參數(shù),不過這個可以通過自定義option方法來解決;采用接口的實現(xiàn)方式更加靈活,每一個option都可以做精細化設(shè)計,不需要export對象中的field,并且很容易進行調(diào)試和測試,缺點是需要維護兩套結(jié)構(gòu),當(dāng)對象結(jié)構(gòu)發(fā)生變更時,option結(jié)構(gòu)也要變更,增加了代碼維護復(fù)雜性。
實際應(yīng)用中,我們可以自由變化,不能直接定義哪一種實現(xiàn)就是好的,凡事都有兩面性,適合才是最好的。