前言:工作中發現一些服務提供的sdk 真是非常難用,非常讓人困惑,不禁想聊下自己設計sdk 會遵循哪些原則
- 高內聚低耦合
往大點看,我們的庫應該功能內聚,不要有業務侵入,往小點看我們的sdk,需要單一職責。先舉個真實的反例:
custom.Trace("xxx", "xxx")
custom.Debug("xxx", "xxx")
custom.Info("xxx", "xxx")
我司使用最多的日誌庫,竟然把特定業務 format 格式做了個default 句柄,然後放日誌庫裏,做全局方法使用,README 也把這種類型放進去。然後只要是新使用的同學,看README後,都會跑來問我們,這個custom 什麼意思,什麼情況使用。這種設計是明顯不合理的,給自己找麻煩,給別人輸出疑惑。
- 兼容性
每次改動發佈是否符合最小語義化版本管理:https://semver.org/lang/zh-CN/。
有很長一段時間,我們公司沒有版本管理,每次發佈一次lib,就可能導致線上不知道哪裏會出現問題。我們部門維護的sdk又很多,有不少在公司都幾百個庫在同時使用,我怎樣確認我的這次修改發佈不會給線上帶來問題?後來我們引入了版本管理:
版本格式:主版本號.次版本號.修訂號,版本號遞增規則如下:
- 主版本號:當你做了不兼容的 API 修改,
- 次版本號:當你做了向下兼容的功能性新增,
- 修訂號:當你做了向下兼容的問題修正。
舉些例子,我添加了個private 的方法,我該怎樣發版?我添加了個public 屬性的方法我該怎樣發版?我將一個public 的方法加了一個參數又該怎樣發版,如果這個參數是defualt 呢?諸如此類,我們線上的每一次發佈,都需要思考兼容性。
- 可測試性
工程代碼中,單測,benchmark 是比較基礎的要求,測試覆蓋度不到70%的sdk 安全性是很難保證的。通過提高測試覆蓋率,我們確實自己解決了不少低級的bug。
可測試性需要包括data race 的測試。特別是含有併發的語言,比如java,golang,c,c++ 等,任何data race 都是bug,每次上線都應該包含data race 檢測。舉個例子感受下詭異行爲:
package main
import (
"fmt"
"runtime"
"time"
)
var i = 0
func main() {
runtime.GOMAXPROCS(2)
go func() {
for {
fmt.Println("i is", i)
time.Sleep(time.Second)
}
}()
for {
i += 1
}
}
可測試性還包括有效的錯誤處理和關鍵位置的debug日誌。比如向下面的三種常見錯誤處理方式:
try {
//do someing
}
catch (SomeException e){
logger.error()
}
try {
//do someing
}
catch (SomeException e){
// ignore
}
try {
//do someing
}
catch (SomeException e){
// xxx
throw e
}
第二種處理方式,空白的錯誤處理,不打任何日誌,不拋異常,簡直就是線上的噩夢。
- 安全性
安全性主要考慮的幾個點是線程安全、原子性、防重入、阻塞還是非阻塞。
舉個例子,redis https://github.com/gomodule/redigo 的Get 方法,不同的對象行爲不一樣,所有的Get 方法是線程安全的嗎?Pool 的Get 方法是線程安全的嗎?再或者不同的redis sdk 都一定有線程安全的Get 方法嗎?
再看個例子,下面的syncAndReturnAll 方法是原子的嗎?如果我需要原子取該怎樣寫?
//換成真實的redis實例
Jedis jedis = new Jedis();
//獲取管道
Pipeline p = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
p.get(i + "");
}
//獲取結果
List<Object> results = p.syncAndReturnAll();
關於防重入,初始化方法、註冊方法最容易資源泄露,比如下面兩種寫法,假如newClient 不是防重入的,有什麼區別,會造成什麼後果:
func NewBonusClient(disfName string) (*Client, error) {
once.Do(func() {
BonusClient, err = newClient(disfName)
})
return BonusClient, err
}
func NewBonusClient(disfName string) (*Client, error) {
BonusClient, err = newClient(disfName)
return BonusClient, err
}
- 風格一致性
風格一致主要包含幾個方面,第一個方面是,儘量遵循語言本身的規範,比如php 的PSR,python 的pep8 等;第二個方面是項目本身儘量保持風格一致,比如大小寫,命名風格,代碼組織風格等等。
- 好的文檔
究竟一個什麼樣的文檔是好的文檔?我個人認爲,發佈的任何sdk,不同基礎的人都照着文檔能看明白,不會有疑問來找自己,這種文檔就是比較好的文檔。
總結:寫可讀性強、健壯的代碼,是對自己負責,也是對同事負責。
參考:
1,https://azure.github.io/azure-sdk/general_introduction.html#diagnosable
2,https://semver.org/lang/zh-CN/
3,https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/
4,https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design