从别人的代码中吸收营养!让本身成长git
最近使用 GRPC
发现一个设计特别好的地方,很是值得借鉴。github
咱们在平常写方法的时候,但愿给某个字段设置一个默认值,不须要定制化的场景就不传这个参数,可是 Golang
却没有提供像 PHP
、Python
这种动态语言设置方法参数默认值的能力。golang
以一个购物车举例。好比我有下面这样一个购物车的结构体,其中 CartExts
是扩展属性,它有本身的默认值,使用者但愿若是不改变默认值时就不传该参数。可是因为 Golang
没法在参数中设置默认值,只有如下几个选择:编程
ext
字段都作为参数,若是不须要的时候传该类型的零值,这把复杂度暴露给调用者;ext
这个结构体作为一个参数在初始化函数中,与 1
同样,复杂度在于调用者;下面看下代码具体会怎么作安全
const ( CommonCart = "common" BuyNowCart = "buyNow" ) type CartExts struct { CartType string TTL time.Duration } type DemoCart struct { UserID string ItemID string Sku int64 Ext CartExts } var DefaultExt = CartExts{ CartType: CommonCart, // 默认是普通购物车类型 TTL: time.Minute * 60, // 默认 60min 过时 } // 方式一:每一个扩展数据都作为参数 func NewCart(userID string, Sku int64, TTL time.Duration, cartType string) *DemoCart { ext := DefaultExt if TTL > 0 { ext.TTL = TTL } if cartType == BuyNowCart { ext.CartType = cartType } return &DemoCart{ UserID: userID, Sku: Sku, Ext: ext, } } // 方式二:多个场景的独立初始化函数;方式二会依赖一个基础的函数 func NewCartScenes01(userID string, Sku int64, cartType string) *DemoCart { return NewCart(userID, Sku, time.Minute*60, cartType) } func NewCartScenes02(userID string, Sku int64, TTL time.Duration) *DemoCart { return NewCart(userID, Sku, TTL, "") } 复制代码
上面的代码看起来没什么问题,可是咱们设计代码最重要的考虑就是稳定与变化,咱们须要作到 对扩展开放,对修改关闭 以及代码的 高内聚。那么若是是上面的代码,你在 CartExts
增长了一个字段或者减小了一个字段。是否是每一个地方都须要进行修改呢?又或者 CartExts
若是有很是多的字段,这个不一样场景的构造函数是否是得写很是多个?因此简要概述一下上面的办法存在的问题。markdown
CartExts
字段进行扩展;CartExts
字段很是多,构造函数参数很长,难看、难维护;NewCart
中,面条代码不优雅;CartExts
作为参数的方式,那么就将过多的细节暴露给了调用者。接下来咱们来看看 GRPC
是怎么作的,学习优秀的范例,提高自个人代码能力。app
从这你也能够体会到代码功底牛逼的人,代码就是写的美!函数
源码来自:grpc@v1.28.1 版本。为了突出主要目标,对代码进行了必要的删减。oop
// dialOptions 详细定义在 google.golang.org/grpc/dialoptions.go type dialOptions struct { // ... ... insecure bool timeout time.Duration // ... ... } // ClientConn 详细定义在 google.golang.org/grpc/clientconn.go type ClientConn struct { // ... ... authority string dopts dialOptions // 这是咱们关注的重点,全部可选项字段都在这里 csMgr *connectivityStateManager // ... ... } // 建立一个 grpc 连接 func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) { cc := &ClientConn{ target: target, csMgr: &connectivityStateManager{}, conns: make(map[*addrConn]struct{}), dopts: defaultDialOptions(), // 默认值选项 blockingpicker: newPickerWrapper(), czData: new(channelzData), firstResolveEvent: grpcsync.NewEvent(), } // ... ... // 修改改选为用户的默认值 for _, opt := range opts { opt.apply(&cc.dopts) } // ... ... } 复制代码
上面代码的含义很是明确,能够认为 DialContext
函数是一个 grpc 连接的建立函数,它内部主要是构建 ClientConn
这个结构体,并作为返回值。defaultDialOptions
函数返回的是系统提供给 dopts 字段的默认值,若是用户想要自定义可选属性,能够经过可变参数 opts 来控制。学习
通过上面的改进,咱们惊奇的发现,这个构造函数很是的优美,不管 dopts 字段如何增减,构造函数不须要改动;defaultDialOptions
也能够从一个公有字段变为一个私有字段,更加对内聚,对调用者友好。
那么这一切是怎么实现的?下面咱们一块儿学习这个实现思路。
首先,这里的第一个技术点是,DialOption
这个参数类型。咱们经过可选参数方式优化了可选项字段修改时就要增长构造函数参数的尴尬,可是要作到这一点就须要确保可选字段的类型一致,实际工做中这是不可能的。因此又使出了程序界最高手段,一层实现不了,就加一层。
经过这个接口类型,实现了对各个不一样字段类型的统一,让构造函数入参简化。来看一下这个接口。
type DialOption interface { apply(*dialOptions) } 复制代码
这个接口有一个方法,其参数是 *dialOptions
类型,咱们经过上面 for 循环处的代码也能够看到,传入的是 &cc.dopts
。简单说就是把要修改的对象传入进来。apply
方法内部实现了具体的修改逻辑。
那么,这既然是一个接口,必然有具体的实现。来看一下实现。
// 空实现,什么也不作 type EmptyDialOption struct{} func (EmptyDialOption) apply(*dialOptions) {} // 用到最多的地方,重点讲 type funcDialOption struct { f func(*dialOptions) } func (fdo *funcDialOption) apply(do *dialOptions) { fdo.f(do) } func newFuncDialOption(f func(*dialOptions)) *funcDialOption { return &funcDialOption{ f: f, } } 复制代码
咱们重点说 funcDialOption
这个实现。这算是一个高级用法,体现了在 Golang 里边函数是 一等公民。它有一个构造函数,以及实现了 DialOption
接口。
newFuncDialOption
构造函数接收一个函数作为惟一参数,而后把传入的函数保存到 funcDialOption
的字段 f
上。再来看看这个参数函数的参数类型是 *dialOptions
,与 apply
方法的参数是一致的,这是设计的第二个重点。
如今该看 apply
方法的实现了。它很是简单,其实就是调用构造 funcDialOption
时传入的方法。能够理解为至关于作了一个代理。把 apply
要修改的对象丢到 f
这个方法中。因此重要的逻辑都是咱们传入到 newFuncDialOption
这个函数的参数方法实现的。
如今来看看 grpc 内部有哪些地方调用了 newFuncDialOption
这个构造方法。
因为 newFuncDialOption
返回的 *funcDialOption
实现了 DialOption
接口,所以关注哪些地方调用了它,就能够顺藤摸瓜的找到咱们最初 grpc.DialContext
构造函数 opts 能够传入的参数。
调用了该方法的地方很是多,咱们只关注文章中列出的两个字段对应的方法:
insecure
与timeout
。
// 如下方法详细定义在 google.golang.org/grpc/dialoptions.go // 开启不安全传输 func WithInsecure() DialOption { return newFuncDialOption(func(o *dialOptions) { o.insecure = true }) } // 设置 timeout func WithTimeout(d time.Duration) DialOption { return newFuncDialOption(func(o *dialOptions) { o.timeout = d }) } 复制代码
来体验一下这里的精妙设计:
DialOption
,从而确保了 grpc.DialContext
方法可用可选参数,由于类型都是一致的;*funcDialOption
,可是它实现了接口 DialOption
,这增长了扩展性。完成了上面的程序构建,如今咱们来站在使用的角度,感觉一下这无限的风情。
opts := []grpc.DialOption{ grpc.WithTimeout(1000), grpc.WithInsecure(), } conn, err := grpc.DialContext(context.Background(), target, opts...) // ... ... 复制代码
固然这里要介绍的重点就是 opts
这个 slice ,它的元素就是实现了 DialOption
接口的对象。而上面的两个方法通过包装后都是 *funcDialOption
对象,它实现了 DialOption
接口,所以这些函数调用后的返回值就是这个 slice 的元素。
如今咱们能够进入到 grpc.DialContext
这个方法内部,看到它内部是如何调用的。遍历 opts,而后依次调用 apply
方法完成设置。
// 修改改选为用户的默认值 for _, opt := range opts { opt.apply(&cc.dopts) } 复制代码
通过这样一层层的包装,虽然增长了很多代码量,可是明显可以感觉到整个代码的美感、可扩展性都获得了改善。接下来看一下,咱们本身的 demo 要如何来改善呢?
首先咱们须要对结构体进行改造,将 CartExts
变成 cartExts
, 而且须要设计一个封装类型来包裹全部的扩展字段,并将这个封装类型作为构造函数的可选参数。
const ( CommonCart = "common" BuyNowCart = "buyNow" ) type cartExts struct { CartType string TTL time.Duration } type CartExt interface { apply(*cartExts) } // 这里新增了类型,标记这个函数。相关技巧后面介绍 type tempFunc func(*cartExts) // 实现 CartExt 接口 type funcCartExt struct { f tempFunc } // 实现的接口 func (fdo *funcCartExt) apply(e *cartExts) { fdo.f(e) } func newFuncCartExt(f tempFunc) *funcCartExt { return &funcCartExt{f: f} } type DemoCart struct { UserID string ItemID string Sku int64 Ext cartExts } var DefaultExt = cartExts{ CartType: CommonCart, // 默认是普通购物车类型 TTL: time.Minute * 60, // 默认 60min 过时 } func NewCart(userID string, Sku int64, exts ...CartExt) *DemoCart { c := &DemoCart{ UserID: userID, Sku: Sku, Ext: DefaultExt, // 设置默认值 } // 遍历进行设置 for _, ext := range exts { ext.apply(&c.Ext) } return c } 复制代码
通过这一番折腾,咱们的代码看起来是否是很是像 grpc 的代码了?还差最后一步,须要对 cartExts
的每一个字段包装一个函数。
func WithCartType(cartType string) CartExt { return newFuncCartExt(func(exts *cartExts) { exts.CartType = cartType }) } func WithTTL(d time.Duration) CartExt { return newFuncCartExt(func(exts *cartExts) { exts.TTL = d }) } 复制代码
对于使用者来讲,只需以下处理:
exts := []CartExt{ WithCartType(CommonCart), WithTTL(1000), } NewCart("dayu", 888, exts...) 复制代码
是否是很是简单?咱们再一块儿来总结一下这里代码的构建技巧:
2
中的接口类型;(这一步并不是必须,但这是一种良好的编程风格)按照上面的五步大法,你就可以实现设置默认值的高阶玩法。
若是你喜欢这个类型的文章,欢迎留言点赞!
我的公众号:dayuTalk
GitHub:github.com/helei112g