Go:如何應對不斷膨脹的接口
難怪碼農自嘲是 CRUD boy, 每天確實在不斷的堆屎,在別人的屎山上縫縫補補。下面的案例并沒有 blame 任何人的意思,我也是堆屎工^^ 如有雷同,請勿對號入座
案例
最近讀一個業(yè)務代碼,狀態(tài)機接口定義有 40 個函數(shù),查看 commit log, 初始只有 10 個,每當增加新的業(yè)務需求時,就不斷的在原接口添加
//?OrderManager?handles?operation?on?order?entity
type?OrderManager?interface?{
?LoadOrdersByIDs(ctx?context.Context,?orderIDs?[]string)?([]*dbentity.Order,?error)
??......
??TransitOrdersToState(ctx?context.Context,?orderIDs?[]string,?toState?orderstate.OrderState)?([]*dbentity.Order,?error)
??......
?Stop()?error
}
業(yè)務中很多 interface 都是用來 mock UT, 實現(xiàn)依賴反轉,而不是業(yè)務多態(tài)。OrderManager 就屬于這類,所以接口膨脹后對工程質量影響并不大,就是看著不內聚...
接口為什么要小
The bigger the interface, the weaker the abstraction.
Go Proverbs[1] Rob Pike 提到:接口越大,抽像能力越弱,比如系統(tǒng)庫中的 io.Reader, io.Writer 等等接口定義只有一兩個函數(shù)。為什么說接口要小呢?舉個例子
type?FooBeeper?interface?{
??Bar(s?string)?(string,?error)
??Beep(s?string)?(string,?error)
}
type?thing?struct{}
func?(l?*thing)?Bar(s?string)?(string,?error)?{
??...
}
func?(l?*thing)?Beep(s?string)?(string,?error)?{
??...
}
type?differentThing?struct{}
func?(l?*differentThing)?Bar(s?string)?(string,?error)?{
??...
}
type?anotherThing?struct{}
func?(l?*anotherThing)?Beep(s?string)?(string,?error)?{
??...
}
接口 FooBeeper 定義有兩個函數(shù): Bar, Beep. 由于接口實現(xiàn)是隱式的,我們有如下結論:
thing實現(xiàn)了FooBeeper接口differentThing沒有實現(xiàn),缺少Bar函數(shù)anotherThing同樣沒有實現(xiàn),缺少Beep函數(shù)
但是如果我們把 FooBeeper 打散也多個接口的組合
type?FooBeeper?interface?{
?Bar
?Beep
}
type?Bar?interface?{
?Bar(s?string)?(string,?error)
}
type?Beep?interface?{
?Beep(s?string)?(string,?error)
}
如上述定義,就可以將接口做小,使得 differentThing anotherThing 可以復用接口
組合改造
關于如何改造 OrderManger 可以借鑒 etcd client v3[2] 定義的思想,將相關的功能聚合成小接口,通過接口的組合實現(xiàn)
type?Client?struct?{
?Cluster
?KV
?Lease
?Watcher
?Auth
?Maintenance
?conn?*grpc.ClientConn
?cfg??????Config
??......
}
上面是 clientV3 結構體定義,雖然不是接口,但是思想可以借鑒
//?OrderManager?handles?operation?on?order?entity
type?OrderManager?interface?{
?OrderOperator
?TransitOrders
?Stop()?error
}
實際上可能接口只需抽成三個,OrderOperator 負責對 orders 的 CRUD 操作,TransitOrders 負責轉態(tài)機流轉,原來的 40 個函數(shù)函數(shù)都放到小接口里面
冗余改造
只抽成小接口是不行的,LoadOrderByXXXX 有一堆定義,根據(jù)不同條件獲取訂單,但實際上這些都是可以轉換的
func?LoadOrders(ctx?context.Context,?FiltersParams?options...)
針對這種情況可以傳入 option, 或是用結構體當成參數(shù)容器。再比如狀態(tài)機流轉有 TransitOrdersToState, TransitOrdersToStateByEntity, TransitOrdersStateByEntityForRegularDelivery 均屬于冗余定義
還有一種冗余接口是根本沒人用,或是不該這層暴露的
預防
接口拆分本質上是 ISP Interface segregation principle[3], 不應該強迫任何代碼依賴它不使用的方法。IT 行業(yè)有一個笑話
當你的 MR 只有幾行時,peer 會提出幾十個 comment. 但是當你的 MR 幾百行時,他們只會回復 LGTM
Peer review 還是要有責任心的,如果成本不高,建議順手把老代碼重構一下。重構代碼有幾項原則,可以參考 重構最佳實踐2
CI lint 不知道是否支持檢查 interface 行數(shù),但是如果行數(shù)成為指標,可能又本末倒置了
小結
還是那句話,知易行難,從點滴做起吧
寫文章不容易,如果對大家有所幫助和啟發(fā),請大家?guī)兔c擊在看,點贊,分享 三連
關于 接口 大家有什么看法,歡迎留言一起討論,大牛多留言 ^_^
參考資料
Go Proverbs: https://go-proverbs.github.io/,
[2]etcd client v3: https://github.com/etcd-io/etcd/blob/main/client/v3/client.go#L44,
[3]Interface segregation principle: https://en.wikipedia.org/wiki/Interface_segregation_principle,
推薦閱讀
