奇葩的sdk?sdk 設計該思考的一些原則

前言:工作中發現一些服務提供的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又很多,有不少在公司都幾百個庫在同時使用,我怎樣確認我的這次修改發佈不會給線上帶來問題?後來我們引入了版本管理:

版本格式:主版本號.次版本號.修訂號,版本號遞增規則如下:

  1. 主版本號:當你做了不兼容的 API 修改,
  2. 次版本號:當你做了向下兼容的功能性新增,
  3. 修訂號:當你做了向下兼容的問題修正。

舉些例子,我添加了個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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章