共计 10784 个字符,预计需要花费 27 分钟才能阅读完成。
全文 10878 字,预计浏览工夫 26 分钟
一、背景介绍
在平时工作中,咱们会把通用的代码,合并到一个通用的 SDK 中,减少大家工作效率,本文次要分享咱们在编写 SDK 时候的准入规范以及相干编码思维。
首先须要答复,为什么要编写 SDK?
1. 防止反复造轮子
2. 缩小线上 bug 概率
1.1 防止反复造轮子
好的 sdk 能够帮忙团队省时省力,将雷同的性能形象到一个通用 sdk 中,前人栽树后人乘凉。
1.2 缩小线上 bug 概率
1. 通过大家独特的优化出 bug 的可能性较低,即便出 bug,也只须要批改 sdk 即可;
2. 若每个代码库都实现一遍,那每个代码库就都须要修复这个 bug;
3. 每个同学能力不同,写出代码的品质参差不齐;
二、遵循的设计理念 SOLID
在程序设计的畛域内,SOLID 是由 Robert C.Martin 提出的面相对象编程以及面向对象设计的五个根本准则的缩写,这五个准则别离是:
– 繁多职责准则(Single ResponsibilityPrinciple)
– 开闭准则 (Open Close Principle)
– 里氏替换准则 (Liskov Substitution Principle)
– 接口隔离准则 (InterfaceSegregation Principle)
– 依赖反转准则 (Dependence InversionPrinciple)
在编写 SDK 中,咱们深信 好的代码是设计进去的,而不是编写进去 的这一理念,所以在设计之初咱们就会依照这五大准则,逐个考量咱们是否的设计是否足够优良,咱们是否违反了某项准则。
接下来咱们来逐个介绍咱们对于这五大准则的了解:
2.1 繁多职责准则
定义
类的设计,尽量做到只有一个能够引起它变动的起因
了解
繁多职责准则的存在是为了保障对象的 高内聚 、保障同一对象的 细粒度。在程序的设计上,如果咱们采纳了繁多职责准则,那么就要留神,开发代码过程中不要设计那些性能形形色色、蕴含很多很多不相干的性能的类,这样的类咱们能够认为是违反繁多职责准则的。依照繁多职责准则设计的类中,应该有且只有一个扭转因素;
举例子
package phone
// 只负责调整电话音量
type MobileSound interface {
// 音量 +
AddSound() error
// 音量 -
ReduceSound() error}
// 只负责调整照片格局
type MobilePhoto interface{
// 亮度
Brightness() error
// 纵横比
AspectRatio() error}
这段代码是对于手机的两个类的结构,在设计的时候,咱们将手机的音量控制和手机图片的调整类进行了辨别,在同一个类中不同性能的独特载体别离是音量控制和图片调整,这两个类中的性能是不产生依赖的,互相平行的关系,当手机的配置产生扭转,那么这两个类中的性能都会产生肯定的变动;
如果咱们要对其中一个类进行调整,那么只须要在相应的 interface 中进行性能的优化即可,不会影响到其余性能的构造。
2.2 开闭准则
定义:一个软件实体,应该对扩大凋谢,对批改敞开;
了解
针对于这个问题的产生是来自于对于代码进行保护的时候,如果对旧代码进行批改,那么可能会引入谬误,这可能会导致咱们对整个性能进行从新架构,并且从新测试;
为了避免这种状况的产生,咱们能够采纳开闭准则,咱们只对代码进行增加性能,扩大类中的性能,而不去批改原先的性能,这样能够避开因为批改老代码而产生的坑(深有体会);
举例子
// 只负责调整电话音量, 同时具备一键最大和一键最小的性能
type MobileSound interface {
// 音量 +
AddSound() error
// 音量 -
ReduceSound() error
// 静音
Mute() error
// 一键最大
MaxSound() error}
还是这个手机的类,如果前期业务需要减少,要求咱们退出静音和一键音量最大的性能,那么咱们能够间接在这个音量控制接口中进行性能扩大,而不是去在音量加减的性能里进行批改,这样能够防止因为对外部逻辑的调整而产生的性能架构问题。
留神点
开闭准则是一个十分虚的准则,咱们须要在提前预期好变动并做出布局,而后需要的变动总是远远超过咱们的预期,遵循这个准则咱们应该把频繁变动的局部做出形象,但却不能将每个局部都刻意的进行形象;
如果咱们无奈预期变动,就不要去做刻意的形象,咱们应该回绝并不成熟的形象。
2.3 里氏替换准则
定义
所有援用基类的中央必须能通明的应用其子类的对象;
了解
子类对象替换父类对象的时候,程序自身的逻辑不能发生变化,同时不能对程序的性能造成影响;
举例
依然是用手机来举例子,咱们定义的手机类中,退出了一个充电性能接口;
– 其中蕴含了无线充电和有线充电两个性能;
type Charge interface {
// 有线充电
ChargeWithLine() error
// 无线充电
ChargeWithoutLine() error}
– 然而咱们并没有想到 8848 钛金手机这么一款高端商务上流手机并不具备无线充电的性能;
因为咱们定义的父类并不能由子类齐全替换,8848 也是手机,然而因为它的性能并不齐全具备手机类的性能,所以导致这个问题的产生,那么如何来解决这个问题呢?
咱们能够将手机类的充电性能进行拆分,在父类中,咱们只定义充电性能,那么咱们就能够在 8848 子类外面设计具体的充电形式,来欠缺这个性能的接口。咱们在手机类中仅定义了最根底的办法集,通过子接口 SpecialPhone 增加 ChargeWithLine 办法;
type MobilPhone interface {Name() string
Size() int}
type NormalCharge interface {
MobilPhone
ChargeWithLine() error
ChargeWithoutLine() error}
type SpecialCharge interface {
MobilPhone
ChargeWithLine() error}
– 咱们再通过 SpecialPhone 来提供对 MobilPhone 的根本实现:
type SpecialPhone struct {
name string
size int
}
func NewSpecialPhone(name string, size int) *SpecialPhone {
return &SpecialPhone{
name,
size,
}
}
func (mobile *SpecialPhone) Name() string {return mobile.name}
func (mobile *SpecialPhone) Size() int {return mobile.size}
– 最初,Mobil8848 通过聚合 SpecialPhone 实现 MobilPhone 接口, 通过提供 ChargeWithLine 办法实现 Mobil8848 子接口:
type Mobil8848 struct {SpecialPhone}
func NewMobil8848(name string, size int) MobilPhone {
return &Mobil8848{*NewSpecialPhone(name, size),
}
}
func (mobile *Mobil8848) ChargeWithLine() error {return nil}
留神点
在我的项目中,采纳里氏替换准则时,尽量避免子类的 特殊性,采纳该准则的目标就是减少健壮性,在版本升级时也能有很好的兼容性,而一旦有了特殊性,那么代码间的耦合关系就会变得极其简单;
2.4 接口隔离准则
定义
– 客户端不应该依赖它不须要的接口;
– 类间的依赖关系应该建设在最小的接口上;
了解
– 客户端不能依赖于它所不须要应用的接口,这里的客户端咱们能够了解为接口的调用方或者使用者;
– 尽量保障一个接口对应一个功能模块;
接口隔离准则和繁多指摘准则的区别是什么?
繁多职责准则次要是在业务逻辑层面上,保障一个类对应一个职责,重视的是对于模块的设计;
而接口隔离准则则是次要针对接口的设计,对不同的模块提供不同的接口。
举例
比方上面这个例子,这段代码就是清晰的将接口依照不同的返回值进行了辨别,这样在调用的时候就能够防止因为不分明返回的认证信息状态而造成的逻辑上的谬误。
// CarOwnerCertificationSuccessScheme 返回认证胜利的 scheme
func CarOwnerCertificationSuccessScheme() string {return CertificationSuccessScheme}
// CarOwnerCertificationFailedScheme 返回认证失败的 scheme
func CarOwnerCertificationFailedScheme() string {return CertificationFailedScheme}
// CarOwnerMessageRecallScheme 返回车主认证邀请的 scheme
func CarOwnerMessageRecallScheme() string {return MessageRecallScheme}
留神点
同繁多职责准则相似:
– 在接口设计时,如果粒度太小会导致接口数据太多,开发人员被泛滥的接口吞没;
– 如果粒度太大,又可能导致灵活性升高。无奈反对业务变动;
– 咱们须要深刻理解业务逻辑,不可自觉照抄巨匠的接口;
2.5 依赖倒置准则
定义
高层模块不能依赖底层模块,两者都应该依赖于其形象;
形象不应该依赖细节;
细节应该依赖形象;
了解
1. 好的形象更稳固,能够笼罩到更多的细节;
2. 具体的业务则要面临着各种办法和细节,十分多样,是不确定不稳固的;
举例
比方上面这个例子,如果咱们想要空虚手机的性能,那么咱们在定义的时候,对手机这个类只定义抽象类,类外面并不波及到具体的功能设计,具体的功能设计咱们要放在性能外面,这样就能够保障类构造的稳固,不会因为性能的调整而导致整个手机类的调整。
package phone
type Phone interface{
// 音量调整
MobileSound
// 照片调整
MobilePhoto
}
// 只负责调整电话音量
type MobileSound interface {
// 音量 +
AddSound() error
// 音量 -
ReduceSound() error}
// 只负责调整照片格局
type MobilePhoto interface{
// 亮度
Brightness() error
// 纵横比
AspectRatio() error}
留神点
依赖倒置最难反对就是找到形象,好的形象能够缩小大量的代码,蹩脚的形象,就突增工作量;
对于设计理念,咱们深信好的零碎是设计进去的,而不是开发进去的。所以在任何我的项目的启动开发之前,咱们都会进行极其严格的设计评审。
三、遵循的编码准则
3.1 稳固、高效
公共库是提供给泛滥业务方应用的第三方组件,如果公共库运行时程序解体,会危及业务方的我的项目,可能会造成线上事变,所以稳固是一个公共库的根本保障。
3.2 裸露异样
异样能够通过日志和接口返回裸露给用户。对于异常情况肯定要打日志,不便用户排查具体问题,并且约定错误码,通过错误码和 error message 将错误信息裸露给用户。在公共库外部,所有可能返回谬误的中央都不能疏忽。
参数校验
用户很有可能无心中给你封装的办法传入了不非法的参数,你的办法要能解决任何不非法的参数,让你的公共库不至于因为传入不非法的参数造成解体。
Panic 和 Recover
在 Go 语言中,一些非法的操作会导致程序 panic,例如拜访超出 slice 边界的元素时,通过值为 nil 的指针拜访构造体的字段,敞开曾经敞开的 channel 等。
当一个 panic 在调用栈中的没有被 recover 时,程序终将因堆栈溢出而终止。在编写公共库时,咱们不心愿某个中央出现异常就导致整个程序解体。正确的做法是 recover 这个 panic,转换成 error 返回给调用方,同时输入到日志,使得公共库及整个工程项目还能放弃失常运行。
如示例代码:
func server(workChan <-chan *Work) {
for work := range workChan {go safelyDo(work)
}
}
func safelyDo(work *Work) {defer func() {if err := recover(); err != nil {log.Println("work failed:", err)
}
}()
do(work)
}
在每个对包外公开的函数和办法都应该 recover 到外部的 panic 并且将这些 panic 转换为错误信息,避免外部 panic 造成程序终止.
3.3 测试
封装好一个函数或者功能模块后,咱们须要测试其逻辑的正确性和函数的性能,这就须要单元测试和性能测试。单元测试的重点在于验证程序设计或实现的逻辑是否正确,而压力测试的重点在于测试程序性能,确认程序在高并发的状况下还能保持稳定运行。
单元测试
单元测试就是针对某一个函数或者进行测试,通过各种可能的样例(即函数的输出和冀望的输入)验证函数各分支是否都失去了预期的后果,或者可能的问题都能被预知并提前解决。通过单测能解决以下问题:
– 提前发现发现程序设计或实现上的逻辑谬误,便于及时定位解决,确保每个函数是可运行的,运行后果是正确的。
– 一次编写,屡次运行。编写好一个函数的单元测试后,后续对函数的批改或重构,都能够应用此前编写的单测来确保批改后的函数仍然放弃正确的逻辑,避免手动测试脱漏一些边界状况,给代码留下隐患。
压力测试
压力测试的重点是测试程序在高并发的状况是否还能保持稳定运行,以及测试程序可能接受的并发量。公共库除了保障逻辑正确外,还应该保障其在高并发的状况下还能失常运行。
3.4 放弃向下兼容
公共库应该版本向下兼容,即便更新了公共库版本的用户,其此前应用旧版本公共库的我的项目代码还能失常运行。
– 公共库对外裸露的接口应该防止改变,其函数签名不应该再改变,对函数逻辑的批改应该让用户无感知。
– 如果此前的接口的确无奈满足需要,那么能够降级一个库版本,在新版本中开发一个新的接口,而不是在原版本中间接对接口进行批改。
3.5 缩小内部依赖
公共库应该尽量减少内部依赖,应该防止应用不稳固的内部依赖。在封装公共库时,也应该精简代码与性能,以最精简的状态提供最外围的性能,防止公共库的体积过大
3.6 易用性
对立调用
SDK 应被动封装简单的调用形式,屏蔽底层简单逻辑,尽量提供给应用方简略的调用模式。让使用者能在几行代码中实现性能,缩小使用者在调⽤的流程和对参数的了解老本。同时提供敌对的提醒,便于使用者调用调试。
这些须要在接口设计阶段做到极致,不仅要调用简略,又在提供根底性能的根底上,反对扩大定制化。例如,咱们在工程中,通常会将仅需一次加载,整个工程生命周期失效的参数配置化,以一个配置文件作对立治理。但实际上配置文件的格局分多种,例如 yaml、json、xml 等,可扩展性设计在对配置内容的含意、默认的配置项具体阐明的根底上,反对多类配置文件的加载 …… 等。
在接口设计时对立格调,能够给使用者留下业余的印象,同时它也能够传递出开发者 SDK 的设计理念。当呈现跨平台同性能的应用时,可沿用平台格调,通过连续格调,让使用者更直观的调用 SDK 性能。
3.7 可了解性
目录构造
代码的目录构造能够定义整个 SDK 的档次架构,咱们能够通过目录大抵通晓其外部实现围绕的主题。在目录构造定义上,应命名上间接突出主题,拆分明确,尽量不存在交加,不应让人去猜或等读完源码才晓得在做什么。
好的拆分能防止反复代码并不便使用者查找,例如 log、doc、util、config、test、build 等能让人一眼明确该目录下所做的工作。同时目录档次也尽量不要太深,太深会肯定水平上减轻使用者累赘。
.
├── config // 配置文件
│ ├── agent.json
│ ├── alarm.json
│ ......
├── docs // 阐明文档
│ ├── introduce
│ │ └── index.html
│ ├── api
│ │ └── index.html
│ ├── index.html
│ ├── LICENSE.md
│ ......
── test // 单元测试
│ ├── util
│ │ ├── common
│ │ │ ├── date_test.go
│ │ │ └── md5_test.go
│ ......
├── util // 通用工具办法
│ ├── common
│ │ ├── date.go
│ │ ├── md5.go
│ ├── errno
│ │ └── errno.go
│ ......
├── modules // 外围模块
│ ├── agent // 监控 agent
│ ├── alarm // 告警
│ ......
├── README.md // 代码库阐明
├── script // 脚本文件
│ └── mysql
│ │ └── init-db.sql
│ ├── build.sh
│ ......
├── cmd // 工程启停操作等
│ ├── start.go
│ ├── stop.go
......
对立格调
SDK 反对的性能通常是由多人单干实现,置信每位同学都有本人独有的代码风范。SDK 是对外提供的工具包,是一个整体,若在内容格调上跳跃太大,容易造成不谨严等 …… 的负面体验。好的体验是让使用者在应用或学习过程中无感外部差别。
在这里,咱们次要从代码、正文、文档三方面来介绍:
代码格调
代码的书写格调,常见的如:代码缩进、换行(if…else…、for、函数等的 ”{“ 是否换行)、空行(函数与函数、变量、正文等之间空几行)、命名形式(驼峰还是下划线,转换函数的命名用 AtoB 还是 A2B…)。
// 文件名(驼峰或下划线):diskstats.go / disk_stats.go
// 构造体 SafeAgents 与 变量 Agents 应距离几行
// 函数 Put 与 NewSafeAgents 距离 1 行 or 更多
type SafeAgents struct {
sync.RWMutex
M map[string]*model.AgentUpdateInfo
}
var Agents = NewSafeAgents()
func NewSafeAgents() *SafeAgents {return &SafeAgents{M: make(map[string]*model.AgentUpdateInfo)}
}
func (this *SafeAgents) Put(req *model.AgentReportRequest) {
val := &model.AgentUpdateInfo{LastUpdate: time.Now().Unix(),
ReportRequest: req,
}
......
}
代码格调方面还有其余许多注意事项,此处就不一一介绍了,总体说来,对立代码格调,能让整个 SDK 代码库看起来更整洁。
正文格调
正文是对以后代码的解释,好的正文应让使用者在不浏览源码的状况下,通晓以后代码的用意。正文的内容应简洁明了,突出重点。
SDK 中蕴含的正文类型分多种:
1. 呈现在文件头部的正文,申明以后文件内容的形容、批改信息等;
2. 函数的正文,阐明以后函数的性能、参数、返回值、抛出的异样等;
3. 重要的变量名常量、枚举值的含意正文等;
4. 代码外部重要的逻辑正文,例如算法的实现、重要的条件分支等。
在每一类正文下,会呈现多种正文格调,以函数 ToSlice 与 HistoryData 为例,ToSlice 正文重点阐明函数的性能,HistoryData 用 @param、@return、@desc 非凡符对函数的性能,参数,返回值进行阐明。
C 语言格调的 /* */ 块正文, 也反对 C ++ 格调的 // 行正文
// ToSlice 转换
func (this *SafeLinkedList) ToSlice() []*model.JudgeItem {this.RLock()
defer this.RUnlock()
sz := this.L.Len()
if sz == 0 {return []*model.JudgeItem{}}
ret := make([]*model.JudgeItem, 0, sz)
for e := this.L.Front(); e != nil; e = e.Next() {ret = append(ret, e.Value.(*model.JudgeItem))
}
return ret
}
// @desc 获取历史数据
// @param limit 本次申请至少返回的历史数据量,如果未达到 limit 限度,返回已有的所有数据量
// @return bool isEnough 数据未达到 limit 量,false;func (this *SafeLinkedList) HistoryData(limit int) ([]*model.HistoryData, bool) {
if limit < 1 {
// 其实 limit 不非法,此处也返回 false 吧,下层代码要留神
// 因为 false 通常使下层代码进入异样分支,这样就对立了
return []*model.HistoryData{}, false
}
......
}
咱们不对以上列举的正文格调进行评估,仅举例说明正文格调的差异性。
实际上咱们能够在 SDK 开发启动前,借助第三方工具配置每一类正文模版及快捷键,这样可在肯定水平上束缚开发者,对立正文格调。
文档格调
对于我的项目作者而言,编写我的项目文档的过程中,能够为本人的我的项目设计进行逻辑梳理,为本人的编码逻辑打好草稿。因为批改文档的老本,要比批改代码的成本低的多。
领有好的文档也能够升高我的项目的保护老本,文档能够作为我的项目的保护指南,也便于我的项目的迭代和交接。
对于读者而言,一个好的我的项目文档,能够清晰地获取我的项目作者的思考过程,在波及到我的项目单干沟通沟通的场景下,可能很好地进步交换的效率。
通过我的项目的文档,能够让读者理解:
1. 我的项目的背景
2. 我的项目能够提供什么能力(应用场景)
3. 我的项目可能解决什么问题
4. ……
文档又分为设计文档、用户文档等:
1. 设计文档
1) 需要剖析文档 – 研发对需要的了解
2) 零碎设计文档 – 研发人员设计零碎的思路
3) 接口设计文档 – 研发人员设计接口的思路
4) ……
2. 用户文档
1) 产品应用文档
2) 操作指南
3) ……
3. ……
SDK 文档的保护不仅关系到应用人员的接入体验,同时也能缩小开发人员对业务征询问题的解决累赘。制订规范写作标准和格局标准能减少文档的可了解性。
例如:
1. 格局标准(字体大小色彩、题目、非凡示意 … 等)
2. 图表标准(图表对立用某一软件作图,并在某一目录保留原文件,不便当前保护同学更新 …)
3. 文案标准(禁含混用词、断句、中英标点混用 …)
4. ……
标准的文档更具专业性与权威性。
其实现已有许多成熟的工具包能够帮忙咱们查看 go 代码中的不标准,例如罕用的 Golint,它定义的只能采纳驼峰命名办法,一些非凡的变量必须大写(如 ID、IP…),全局变量 & 函数 & 构造体必须有正文 … 等等,肯定水平上帮忙咱们束缚了编码习惯。在许多工具的抉择和应用中,找到适宜本人的才是最无效的,同时咱们也能够依据团队自有特色制订本人的束缚规范。
四、实践经验
4.1 代码准入规定
所有的 commit, 必须关联外部需要卡片
在外部需要治理中,咱们个别会通过外部工具来治理需要、bug 等信息,每次开发人员提交的 commit 信息大多比拟简洁,无奈从展现出具体的信息,所以咱们要求研发同学在提交代码时,必须关联卡片,不便后续追溯代码、定位问题
引入 golint 代码格调查看, 强制要求开发人员通过 golint 代码格调校验
为保障代码格调对立,外部 sdk 库对立应用 golint 工具进行初步验证,无奈通过 golint 查看的代码无奈合入,从机制上解决代码格调不统一的问题
git 分支版本治理
因为外部 sdk 代码库,需要绝对独立,代码间抵触较少,所以咱们并未强制大家必须拉分支后能力合入,为简化提交规定,小型的代码提交,例如改变某个函数等,能够间接在 master 上进行改变。但大型项目,则必须拉分支
CR 机制,成立外部 sdk 委员会
外部评审较多,且大家都是在工作之余进行代码 cr,若由 1 - 2 名同学负责保护,则难以保障 cr 速度以及品质,所以咱们成立了 CR 委员会,每个委员会的同学都有权限对代码进行 +2(准入)评审权限,但定期 SDK 委员会将会组织 code reivew,以保障代码库的可维护性
五、总结
一个好的 SDK 能够加重工作量、升高平台应用门槛、减少零碎健壮性等多个益处,在本文中咱们别离从设计模式、代码准则、文档、正文等多个方面形容了咱们是如何在百度外部保护好一个 SDK 库,想要写出一个好的代码自身就不是一件容易的事件,须要咱们一直的更新本人的常识体系,继续优化、重构之前的代码。
咱们始终崇尚:每一个我的项目、每一行代码都应该是依照最高的规范去设计以及开发!
咱们也始终致力能写出大师级的代码,当咱们回首本人代码的时候,不因文档正文凌乱而烦恼,也不因蹩脚的实际而懊悔。
作者简介:刘东阳,百度高级研发工程师
特地鸣谢
感激以下相干同学在忙碌的工作之余为本文做出的奉献(排名不分先后): 陈洋、董瑶、孙琳、王海汀
参考文献
- 手撸 golang 架构设计准则 里氏替换准则:
https://www.jianshu.com/p/330…
https://studygolang.com/artic…。
– 设计模式之禅
– 大话设计模式
– https://liwenzhou.com/
– https://draveness.me/golang-101/
– https://eli.thegreenplace.net…
– http://yunxin.163.com/blog/fe…
* SDK 开发教训分享: http://yunxin.163.com/blog/fe…
* https://www.jianshu.com/p/b9d…
* SDK 开发之文档: https://www.jianshu.com/p/953…
* open-falcon(文章局部内容以开源监控框架 open-falcon 作代码示例阐明): https://github.com/open-falco…
举荐浏览:
百度 APP 视频播放中的解码优化
百度爱番番实时 CDP 建设实际
当技术重构遇上 DDD,如何实现业务、技术双赢?
接口文档主动更改?百度程序员开发效率 MAX 的秘诀
技术揭秘!百度搜寻中台低代码的摸索与实际
百度智能云实战——动态文件 CDN 减速
化繁为简 – 百度智能小程序主数据架构实战总结
———- END ———-
百度 Geek 说
百度官网技术公众号上线啦!
技术干货 · 行业资讯 · 线上沙龙 · 行业大会
招聘信息 · 内推信息 · 技术书籍 · 百度周边