奇葩的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又很多,有不少在公司都几百个库在同时使用,我怎样确认我的这次修改发布不会给线上带来问题?后来我们引入了版本管理:
版本格式:主版本号.次版本号.修订号,版本号递增规则如下:
- 主版本号:当你做了不兼容的 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