How to write sdk?

picture

The full text is 10878 words, the estimated reading time is 26 minutes

1. Background introduction

In our daily work, we will merge common codes into a common SDK to increase everyone's work efficiency. This article mainly shares our access standards and related coding ideas when writing SDKs.

First need to answer, why write an SDK?

1. Avoid reinventing the wheel

2. Reduce the probability of online bugs

1.1 Avoid reinventing the wheel

A good sdk can help the team save time and effort, and abstract the same functions into a general sdk, so that predecessors can plant trees and others can enjoy the shade.

1.2 Reduce the probability of online bugs

1. After everyone's common optimization, the possibility of bugs is low. Even if there are bugs, you only need to modify the sdk;

2. If each code base is implemented once, then each code base needs to fix this bug;

3. Each student has different abilities, and the quality of the code written is uneven;

2. Follow the design concept SOLID

In the field of programming, SOLID is an abbreviation for the five basic principles of object-oriented programming and object-oriented design proposed by Robert C. Martin. The five principles are:

- Single Responsibility Principle (Single Responsibility Principle)

- Open Close Principle

- Liskov Substitution Principle

- Interface Segregation Principle

- Dependence InversionPrinciple

In writing the SDK, we firmly believe that good code is designed, not written , so at the beginning of the design, we will follow these five principles to consider whether our design is good enough and whether we violate it. a certain principle.

Next, let's introduce our understanding of these five principles one by one:

2.1 Single Responsibility Principle

definition

Class design, try to have only one reason that can cause it to change

understand

The single responsibility principle exists to ensure high cohesion of objects and fine-grainedness of the same object . In the design of the program, if we adopt the single responsibility principle, then we should pay attention not to design those classes with various functions and many unrelated functions in the process of developing the code. We can consider such classes to violate the single responsibility principle. of. In a class designed according to the single responsibility principle, there should be only one change factor;

for example

package phone

// 只负责调整电话音量
type MobileSound interface {
    // 音量+
    AddSound() error
    // 音量-
    ReduceSound() error
}
// 只负责调整照片格式
type MobilePhoto interface{
    // 亮度
    Brightness() error
    // 纵横比
    AspectRatio() error
}

This code is for the construction of two classes of mobile phones. When designing, we distinguish the volume control of mobile phones from the adjustment classes of mobile phone pictures. The common carriers of different functions in the same class are volume control and pictures. Adjustment, the functions in these two classes are not dependent and have a parallel relationship with each other. When the configuration of the mobile phone changes, the functions in these two classes will change to a certain extent;

If we want to adjust one of the classes, we only need to optimize the function in the corresponding interface, and it will not affect the structure of other functions.

2.2 The open-closed principle

Definition: A software entity that should be open for extension and closed for modification;

understand

The reason for this problem comes from the maintenance of the code. If the old code is modified, it may introduce errors, which may cause us to re-architect the entire function and re-test;

In order to prevent this from happening, we can adopt the open-closed principle. We only add functions to the code and extend the functions in the class without modifying the original functions, so as to avoid the pits caused by modifying the old code ( deep experience);

for example

// 只负责调整电话音量,同时具备一键最大和一键最小的功能
type MobileSound interface {
    // 音量+
    AddSound() error
    // 音量-
    ReduceSound() error
    // 静音
    Mute() error
    // 一键最大
    MaxSound() error
}

It is still the class of this mobile phone. If the later business needs increase and require us to add the functions of mute and one-button volume maximum, then we can directly expand the function in this volume control interface, instead of modifying it in the function of volume addition and subtraction. , which can avoid functional architecture problems caused by adjustments to internal logic.

important point

The open-closed principle is a very imaginary principle. We need to anticipate changes and make plans in advance, and then the changes in demand are always far beyond our expectations. Following this principle, we should abstract the frequently changing parts, but However, each part cannot be deliberately abstracted;

If we can't anticipate change, don't make deliberate abstractions, we should reject immature abstractions.

2.3 The Liskov Substitution Principle

definition

All references to the base class must transparently use objects of its subclasses;

understand

When the subclass object replaces the parent class object, the logic of the program itself cannot be changed, and the function of the program cannot be affected at the same time;

Example

Still using a mobile phone as an example, a charging function interface is added to the mobile phone class we defined;

- It includes two functions of wireless charging and wired charging;

type Charge interface {
    //有线充电
    ChargeWithLine() error
    //无线充电
    ChargeWithoutLine() error
}

- But we did not expect that a high-end business mobile phone such as the 8848 titanium mobile phone does not have the function of wireless charging;

Because the parent class we define cannot be completely replaced by the subclass, the 8848 is also a mobile phone, but because its functions do not fully have the functions of the mobile phone class, this problem occurs, so how to solve this problem?

We can split the charging function of the mobile phone class. In the parent class, we only define the charging function, then we can design a specific charging method in the 8848 subclass to improve the interface of this function. We only define the most basic method set in the mobile phone class, and add the ChargeWithLine method through the sub-interface SpecialPhone;

type MobilPhone interface {
    Name() string
    Size() int
}

type NormalCharge interface {
    MobilPhone
    ChargeWithLine() error
    ChargeWithoutLine() error
}

type SpecialCharge interface {
    MobilPhone
    ChargeWithLine() error
}

- We then provide the basic implementation of MobilPhone through SpecialPhone:

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
}

- Finally, Mobil8848 implements the MobilPhone interface by aggregating SpecialPhone, and implements the Mobil8848 sub-interface by providing the ChargeWithLine method:

type Mobil8848 struct {
    SpecialPhone
}

func NewMobil8848(name string, size int) MobilPhone {
    return &Mobil8848{
        *NewSpecialPhone(name, size),
    }
}

func (mobile *Mobil8848) ChargeWithLine() error {
    return nil
}

important point

In the project, when adopting the Liskov substitution principle, try to avoid the particularity of subclasses. The purpose of adopting this principle is to increase the robustness and have good compatibility during version upgrades. Once there is particularity, then The coupling relationship between codes will become extremely complex;

2.4 Principle of Interface Isolation

definition

- the client should not rely on interfaces it does not need;

- Dependencies between classes should be established on the smallest interface;

understand

- The client cannot rely on the interface it does not need to use. The client here can be understood as the caller or user of the interface;

- Try to ensure that one interface corresponds to one functional module;

What is the difference between the Interface Segregation Principle and the Single Blame Principle?

The single responsibility principle is mainly at the level of business logic to ensure that one class corresponds to one responsibility, focusing on the design of modules;

The interface isolation principle is mainly aimed at the design of the interface, providing different interfaces for different modules.

Example

For example, in the following example, this code clearly distinguishes the interface according to different return values, so that logical errors caused by unclear returned authentication information status can be avoided when calling.

// CarOwnerCertificationSuccessScheme 返回认证成功的scheme
func CarOwnerCertificationSuccessScheme() string {
    return CertificationSuccessScheme
}

// CarOwnerCertificationFailedScheme 返回认证失败的scheme
func CarOwnerCertificationFailedScheme() string {
    return CertificationFailedScheme
}

// CarOwnerMessageRecallScheme 返回车主认证邀请的scheme
func CarOwnerMessageRecallScheme() string {
    return MessageRecallScheme
}

important point

Similar to the Single Responsibility Principle:

- During interface design, if the granularity is too small, there will be too much interface data, and developers will be overwhelmed by numerous interfaces;

- If the granularity is too large, it may lead to less flexibility. inability to support business changes;

- We need to have a deep understanding of the business logic and not blindly copy the master's interface;

2.5 Dependency Inversion Principle

definition

High-level modules cannot depend on low-level modules, both should depend on their abstractions;

Abstraction should not depend on details;

Details should rely on abstractions;

understand

1. A good abstraction is more stable and can cover more details;

2. The specific business has to face various methods and details, which are very diverse and uncertain and unstable;

Example

For example, in the following example, if we want to enrich the functions of the mobile phone, then we only define the abstract class for the mobile phone class when we define it, and the class does not involve specific functional design. The specific functional design should be placed in the function In this way, the stability of the class structure can be ensured, and the adjustment of the entire mobile phone class will not be caused by the adjustment of functions.

package phone

type Phone interface{
    // 音量调整
    MobileSound

    // 照片调整
    MobilePhoto
}

// 只负责调整电话音量
type MobileSound interface {
    // 音量+
    AddSound() error
    // 音量-
    ReduceSound() error
}
// 只负责调整照片格式
type MobilePhoto interface{
    // 亮度
    Brightness() error
    // 纵横比
    AspectRatio() error
}

important point

The hardest way to support dependency inversion is to find abstractions. A good abstraction can reduce a lot of code, and a bad abstraction can suddenly increase the workload;

Regarding design philosophy, we firmly believe that good systems are designed, not developed. So before any project starts development, we conduct an extremely rigorous design review.

3. Coding principles to follow

3.1 Stable and efficient

The public library is a third-party component provided to many business parties. If the program crashes when the public library is running, it will endanger the business party's project and may cause online accidents. Therefore, stability is the basic guarantee of a public library.

3.2 Expose exceptions

Exceptions can be exposed to users through logs and interface returns. Logs must be logged for abnormal situations to facilitate users to troubleshoot specific problems, and an error code should be agreed upon to expose error information to users through error code and error message. Inside the common library, everything that might return an error cannot be ignored.

parameter verification

It is very likely that users accidentally pass illegal parameters to your encapsulated method. Your method must be able to handle any illegal parameters, so that your public library will not crash due to passing in illegal parameters.

Panic and Recover

In the Go language, some illegal operations will cause the program to panic, such as accessing an element beyond the slice boundary, accessing a field of a structure through a pointer with a value of nil, closing a channel that has been closed, and so on.

When a panic in the call stack is not recovered, the program will eventually terminate due to stack overflow. When writing a common library, we don't want an exception somewhere that will crash the entire program. The correct way is to recover the panic, convert it into an error and return it to the caller, and output it to the log at the same time, so that the common library and the entire project can keep running normally.

Such as sample code:

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)
}

Every function and method exposed outside the package should recover the internal panic and convert these panics into error messages to prevent the internal panic from causing the program to terminate.

3.3 Testing

After encapsulating a function or functional module, we need to test the correctness of its logic and the performance of the function, which requires unit testing and performance testing. The focus of unit testing is to verify whether the logic of program design or implementation is correct, while the focus of stress testing is to test program performance and confirm that the program can run stably in the case of high concurrency.

unit test

Unit testing is to test a function or test, through various possible samples (that is, the input and expected output of the function) to verify whether each branch of the function has obtained the expected results, or possible problems can be predicted and advanced. deal with. The following problems can be solved by a single test:

- Detect logic errors in program design or implementation early, so as to locate and solve them in time, and ensure that each function is runnable and the running result is correct.

- Write once, run many times. After writing the unit test of a function, the subsequent modification or refactoring of the function can use the previously written unit test to ensure that the modified function still maintains the correct logic, preventing manual testing from missing some boundary conditions and leaving the code behind hidden danger.

pressure test

The focus of stress testing is to test whether the program can still run stably under high concurrency conditions, as well as the amount of concurrency that the test program can withstand. In addition to ensuring correct logic, the public library should also ensure that it can run normally under high concurrency.

3.4 Maintain backward compatibility

The public library should be backward compatible, even if the user has updated the public library version, the project code that used the old version of the public library can still run normally.

- The interface exposed by the public library should be avoided, its function signature should not be changed, and the modification of the function logic should be invisible to the user.

- If the previous interface does not meet the needs, you can upgrade a library version and develop a new interface in the new version instead of directly modifying the interface in the original version.

3.5 Reduce external dependencies

Common libraries should minimize external dependencies and should avoid using unstable external dependencies. When encapsulating the public library, the code and functions should also be simplified, and the most core functions should be provided in the most streamlined form to avoid the excessive size of the public library.

3.6 Ease of use

Unified call

The SDK should actively encapsulate complex calling methods, shield the underlying complex logic, and try to provide users with a simple calling method. Allow users to implement functions in a few lines of code, reducing the user's calling process and the cost of understanding parameters. At the same time, it provides friendly prompts, which is convenient for users to call and debug.

These need to be perfected in the interface design stage, not only to be simple to call, but also to support extended customization on the basis of providing basic functions. For example, in a project, we usually configure the parameters that only need to be loaded once and take effect in the entire project life cycle, and use a configuration file for unified management. But in fact, there are many formats of configuration files, such as yaml, json, xml, etc. The scalability design is based on the meaning of the configuration content and the detailed description of the default configuration items, and supports the loading of multiple types of configuration files... ...Wait.

The unified style in interface design can leave a professional impression to users, and it can also convey the design concept of the developer SDK. When using the same function across platforms, the platform style can be used, and by continuing the style, users can call the SDK functions more intuitively.

3.7 Understandability

Directory Structure

The directory structure of the code can define the hierarchical structure of the entire SDK, and we can roughly know the topics surrounding its internal implementation through the directory. In the definition of the directory structure, the naming should directly highlight the theme, the division should be clear, and there should be no intersection as much as possible, and people should not guess or wait until they read the source code to know what they are doing.

A good split can avoid duplication of code and facilitate users to find, such as log, doc, util, config, test, build, etc., which can make people clear at a glance the work done in the directory. At the same time, the directory hierarchy should not be too deep, because too deep will increase the burden on users to a certain extent.

.
├── 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
......

Unified style

The functions supported by the SDK are usually completed by multiple people. I believe that each student has his own unique code style. The SDK is a toolkit provided to the outside world, and it is a whole. If the content style jumps too much, it is easy to cause a negative experience such as impreciseness and so on. A good experience is to let users feel no internal differences in the use or learning process.

Here, we mainly introduce from three aspects: code, comments, and documentation:

code style

The writing style of the code, such as: code indentation, line break (if...else..., for, function, etc. "{" whether to wrap the line), blank line (a few lines between functions and functions, variables, comments, etc.), Naming method (camel case or underscore, AtoB or A2B for naming conversion functions...).

// 文件名(驼峰或下划线):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,
  }
  ......
}

There are many other considerations in terms of code style, which will not be introduced here. Generally speaking, unifying the code style can make the entire SDK code base look cleaner.

Annotation style

A comment is an explanation of the current code. A good comment should let the user know the intent of the current code without reading the source code. The content of the notes should be concise and clear, highlighting the key points.

There are several types of annotations included in the SDK:

1. A comment that appears in the header of the file, stating the description and modification information of the current file content;

2. The comment of the function, explaining the function, parameters, return value, thrown exception, etc. of the current function;

3. Important variable name constants, comments on the meaning of enumeration values, etc.;

4. Important logical comments inside the code, such as algorithm implementation, important conditional branches, etc.

Under each type of annotation, there will be a variety of annotation styles. Take the functions ToSlice and HistoryData as examples. The ToSlice annotation focuses on the function of the function. HistoryData uses the @param, @return, and @desc special symbols to describe the function, parameter, and return of the function. value is explained.

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
  }
  ......
}

We do not evaluate the annotation styles listed above, but only exemplify the differences in annotation styles.

In fact, we can configure each type of annotation template and shortcut keys with the help of third-party tools before the SDK development starts, which can constrain developers to a certain extent and unify the annotation style.

document style

For project authors, in the process of writing project documents, they can logically sort out their own project design and make drafts for their own coding logic. Because the cost of modifying the document is much lower than the cost of modifying the code.

Having good documentation can also reduce the maintenance cost of the project. The documentation can be used as a maintenance guide for the project, and it is also convenient for the iteration and handover of the project.

For readers, a good project document can clearly capture the thinking process of the project author, and can improve the efficiency of communication in scenarios involving project cooperation and communication.

Through the project's documentation, readers can understand:

1. Background of the project

2. What capabilities can the project provide (use scenarios)

3. What problems can the project solve?

4. ......

Documents are divided into design documents, user documents, etc.:

1. Design Documentation

  1. Requirements Analysis Document - R&D's understanding of requirements

  2. System Design Documents - R&D staff's ideas for designing systems

  3. Interface Design Documents - R&D staff's ideas for designing interfaces

  4. ......

2. User Documentation

  1. product documentation

  2. Operation guide

  3. ......

3. ......

The maintenance of the SDK documentation is not only related to the user's access experience, but also reduces the developer's burden of dealing with business consulting issues. Establishing standard writing and formatting guidelines can increase the comprehensibility of a document.

E.g:

1. Format specification (font size color, title, special representation...etc)

2. Chart specification (the chart is drawn with a certain software uniformly, and the original file is saved in a certain directory, which is convenient for maintenance and updating by students in the future...)

3. Copywriting specification (prohibition of ambiguous words, punctuation, mixed use of Chinese and English punctuation...)

4. ......

Normative documents are more professional and authoritative.

In fact, there are many mature toolkits that can help us check irregularities in go code, such as the commonly used Golint, which can only be defined in camel case, and some special variables must be capitalized (such as ID, IP...) , global variables & functions & structures must have comments... etc., to a certain extent, it helps us constrain our coding habits. In the selection and use of many tools, it is most effective to find the one that suits you. At the same time, we can also formulate our own constraints according to the team's own characteristics.

4. Practical experience

4.1 Code Access Rules

All commits must be associated with internal requirement cards

In internal requirements management, we generally manage requirements, bugs and other information through internal tools. Most of the commit information submitted by developers is relatively simple and cannot display detailed information. Therefore, we require R&D students to submit code. Cards must be associated to facilitate subsequent tracing of codes and locating problems

Introduce golint code style check, forcing developers to pass golint code style check

In order to ensure the uniform code style, the internal sdk library uses the golint tool for preliminary verification, and the code that cannot pass the golint check cannot be merged, which solves the problem of inconsistent code style from the mechanism.

git branch version management

Due to the internal sdk code base, the requirements are relatively independent and there are few conflicts between the codes, so we do not force you to pull the branch before merging. In order to simplify the submission rules, small code submissions, such as changing a function, can be directly in the Make changes on the master. But for large projects, you must pull the branch

CR mechanism, establish internal sdk committee

There are many internal reviews, and everyone is doing code cr in their spare time. If 1-2 students are responsible for the maintenance, it is difficult to ensure the speed and quality of cr +2 (admission) review authority for code, but regular SDK committee will organize code reivew to ensure the maintainability of the code base

V. Summary

A good SDK can reduce workload, lower the threshold for platform use, and increase system robustness. In this article, we describe how we work within Baidu from the aspects of design patterns, code principles, documentation, and comments. To maintain an SDK library, it is not easy to write a good code. We need to constantly update our knowledge system, continuously optimize and refactor the previous code.

We always advocate: every project and every line of code should be designed and developed according to the highest standards!

We've also worked hard to write master code, and when we look back at our code, we don't feel annoyed by cluttered documentation comments or regret for bad practices.

About the author: Liu Dongyang, senior R&D engineer at Baidu

Special thanks

Thanks to the following related students for their contributions to this article in their spare time (in no particular order): Chen Yang, Dong Yao, Sun Lin, Wang Haiting

references

- Hands-on golang architecture design principles Liskov replacement principle:

https://www.jianshu.com/p/33056c9018ef

https://studygolang.com/articles/32928?fr=sidebar。

- Zen of Design Patterns

- Big talk design mode

- https://liwenzhou.com/

- https://draveness.me/golang-101/

- https://eli.thegreenplace.net/2018/on-the-uses-and-misuses-of-panics-in-go/

- http://yunxin.163.com/blog/fenxiang/

* SDK development experience sharing: http://yunxin.163.com/blog/fenxiang/

* https://www.jianshu.com/p/b9d57514daa0

* SDK development documentation: https://www.jianshu.com/p/953a2b37e587

* open-falcon (part of the article uses the open-source monitoring framework open-falcon as a code example): https://github.com/open-falcon/falcon-plus/tree/master/docs

Recommended reading :

Decoding optimization in Baidu APP video playback

Baidu Aifanfan real-time CDP construction practice

When technical reconstruction meets DDD, how to achieve a win-win situation for business and technology?

Interface documentation changes automatically? The Secret of Baidu Programmer's Development Efficiency MAX

Technology revealed! The exploration and practice of low-code in Baidu search

Baidu Smart Cloud Practice - Static File CDN Acceleration

Simplify complexity--A summary of the main data architecture of Baidu's smart applet

---------- END ----------

Baidu Geek says

Baidu's official technical public account is online!

Technical dry goods·Industry information·Online salon·Industry conference

Recruitment information · Internal push information · Technical books · Baidu peripherals

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324124018&siteId=291194637