如何應(yīng)對(duì)不斷膨脹的接口
本文轉(zhuǎn)載自微信公眾號(hào)「董澤潤(rùn)的技術(shù)筆記」,作者董澤潤(rùn)。轉(zhuǎn)載本文請(qǐng)聯(lián)系董澤潤(rùn)的技術(shù)筆記公眾號(hào)。
難怪碼農(nóng)自嘲是 CRUD boy, 每天確實(shí)在不斷的堆屎,在別人的屎山上縫縫補(bǔ)補(bǔ)。下面的案例并沒(méi)有 blame 任何人的意思,我也是堆屎工^^ 如有雷同,請(qǐng)勿對(duì)號(hào)入座
案例
最近讀一個(gè)業(yè)務(wù)代碼,狀態(tài)機(jī)接口定義有 40 個(gè)函數(shù),查看 commit log, 初始只有 10 個(gè),每當(dāng)增加新的業(yè)務(wù)需求時(shí),就不斷的在原接口添加
- // 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è)務(wù)中很多 interface 都是用來(lái) mock UT, 實(shí)現(xiàn)依賴反轉(zhuǎn),而不是業(yè)務(wù)多態(tài)。OrderManager 就屬于這類,所以接口膨脹后對(duì)工程質(zhì)量影響并不大,就是看著不內(nèi)聚...
接口為什么要小
The bigger the interface, the weaker the abstraction.
Go Proverbs[1] Rob Pike 提到:接口越大,抽像能力越弱,比如系統(tǒng)庫(kù)中的 io.Reader, io.Writer 等等接口定義只有一兩個(gè)函數(shù)。為什么說(shuō)接口要小呢?舉個(gè)例子
- 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 定義有兩個(gè)函數(shù): Bar, Beep. 由于接口實(shí)現(xiàn)是隱式的,我們有如下結(jié)論:
- thing 實(shí)現(xiàn)了 FooBeeper 接口
- differentThing 沒(méi)有實(shí)現(xiàn),缺少 Bar 函數(shù)
- anotherThing 同樣沒(méi)有實(shí)現(xiàn),缺少 Beep 函數(shù)
但是如果我們把 FooBeeper 打散也多個(gè)接口的組合
- type FooBeeper interface {
- Bar
- Beep
- }
- type Bar interface {
- Bar(s string) (string, error)
- }
- type Beep interface {
- Beep(s string) (string, error)
- }
如上述定義,就可以將接口做小,使得 differentThing anotherThing 可以復(fù)用接口
組合改造
關(guān)于如何改造 OrderManger 可以借鑒 etcd client v3[2] 定義的思想,將相關(guān)的功能聚合成小接口,通過(guò)接口的組合實(shí)現(xiàn)
- type Client struct {
- Cluster
- KV
- Lease
- Watcher
- Auth
- Maintenance
- conn *grpc.ClientConn
- cfg Config
- ......
- }
上面是 clientV3 結(jié)構(gòu)體定義,雖然不是接口,但是思想可以借鑒
- // OrderManager handles operation on order entity
- type OrderManager interface {
- OrderOperator
- TransitOrders
- Stop() error
- }
實(shí)際上可能接口只需抽成三個(gè),OrderOperator 負(fù)責(zé)對(duì) orders 的 CRUD 操作,TransitOrders 負(fù)責(zé)轉(zhuǎn)態(tài)機(jī)流轉(zhuǎn),原來(lái)的 40 個(gè)函數(shù)函數(shù)都放到小接口里面
冗余改造
只抽成小接口是不行的,LoadOrderByXXXX 有一堆定義,根據(jù)不同條件獲取訂單,但實(shí)際上這些都是可以轉(zhuǎn)換的
- func LoadOrders(ctx context.Context, FiltersParams options...)
針對(duì)這種情況可以傳入 option, 或是用結(jié)構(gòu)體當(dāng)成參數(shù)容器。再比如狀態(tài)機(jī)流轉(zhuǎn)有 TransitOrdersToState, TransitOrdersToStateByEntity, TransitOrdersStateByEntityForRegularDelivery 均屬于冗余定義
還有一種冗余接口是根本沒(méi)人用,或是不該這層暴露的
預(yù)防
接口拆分本質(zhì)上是 ISP Interface segregation principle[3], 不應(yīng)該強(qiáng)迫任何代碼依賴它不使用的方法。IT 行業(yè)有一個(gè)笑話
當(dāng)你的 MR 只有幾行時(shí),peer 會(huì)提出幾十個(gè) comment. 但是當(dāng)你的 MR 幾百行時(shí),他們只會(huì)回復(fù) LGTM
Peer review 還是要有責(zé)任心的,如果成本不高,建議順手把老代碼重構(gòu)一下。重構(gòu)代碼有幾項(xiàng)原則,可以參考 重構(gòu)最佳實(shí)踐2
CI lint 不知道是否支持檢查 interface 行數(shù),但是如果行數(shù)成為指標(biāo),可能又本末倒置了
參考資料
[1]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,