0xAA55 发表于 2023-11-24 12:05:20

版本管理与接口设计规范

接口的设计是必须跟随版本管理走的。
# 版本管理基础
设计制作软件的时候,应当按照规范的方式设计版本标签,请参考下图。## 概述
版本管理 **并不需要非常死板**,但是必须要有版本管理意识,通过 **合理设计版本号** ,使协作开发人员可以判断你对程序软件的改动大小,并配合你的开发策略跟进开发。
开发人员改代码时,部分代码的改动是为了 **修 bug**,部分代码的改动是为了 **新添功能**,而还有的时候则是重构代码、把旧代码全部删了重新使用新的技术或者策略进行开发。在不同的改动情况下,版本号的递增方式需要经过妥善考虑后进行递增,以便于你的软件的使用者/对接者能够顺利对接你的软件。

# 如何合理设计版本号
此处例举我设计版本号时惯用的一套规则。

## 发布号的使用
开发过程中,软件已经可以使用,可能有 bug,但至少代码已经 **封装好了**、粗略调试可用、等待联调时,可打 Tag。每发布时,Tag 的发布号 **递增**。
接口若发生变化,但是 **仍能兼容旧的接口调用方式** 时,可以使用发布号的递增。但如果接口不再兼容现有的接口调用方式时则需要增加小版本号。根据提交历史,在完成当前需求、发布软件时,打 Tag,并递增发布号。
改到一半、并不能运行的软件 **不打 Tag**。在代码改完、能 **调试运行**时,打 Tag。

## 小版本号的使用
当软件接口发生明显变化,或者软件设计策略发生变化后,发布新的软件时,使小版本号递增,并使 **发布号归零**。
此处描述的设计策略包括 **算法** 的改动、**单线程/多线程运行策略** 的改动、是否使用 **额外资源比如GPU或大量内存、磁盘容量等** 进行计算的改动等。
总结一下通常情况下在此时升高小版本号:
- 软件输出的数据格式发生改变
- 比如输出的 JSON 文件的结构发生改变,旧的读 JSON 方式无法兼容。
- 现有接口发生变化,不再兼容现有调用方式
- 比如 **删除** 了接口函数。
- 改变了接口函数的 **参数** 或 **返回值**。
- 对于命令行程序,**删除或改动**命令行参数对应的软件行为。
- 接口设计模式大幅变化,比如从 **调用子过程**方式进行接口调用变为 **接口类**继承、实现接口类成员函数。
- 完全新增一套新接口
- 比如新增 Web API。
- 在已有 **调用子过程**方式实现功能的基础上,新添一整套的 **接口类**方式接口。
- 一个 DLL 程序,原本只提供了纯 C 语言接口,改动后新增了针对 Java 的接口函数,可用 Java 类封装。
- 一个只能通过读写本地 **文件**来工作的软件变为可以与其它软件直接 **通讯**交换文件内容。
- 软件行为发生明显变化
- 比如本来是执行完成任务后就 **立即结束**的软件变为内存 **驻留待命**的软件。
- 从完全不会产生临时文件的软件变为会 **产生临时文件**的软件。
- 从不需要额外的权限变为有时 **需要额外的权限提升**的软件。
- 单线程软件变为 **多线程**软件。
- 纯本地可运行软件变为 **需要联网**调用某 Web 接口后才能运行的软件。
- 软件若需要按照 **特定结构**来使用文件系统时,文件目录结构发生变化。
- 软件兼容性发生变化
- 比如增加/减少了 **系统软件依赖**。
- 小幅度改变对 **硬件性能**的最低要求。
- 增加/减少对 **网络**的依赖。
- 增加/减少对系统已安装 **字体**的依赖。
- 软件增加了新功能
- 比如某图像压缩软件被设计为既可以压缩图像也可以压缩视频。
- 软件使用的系统资源发生明显变化。
- 比如从只使用 CPU 变为同时使用 CPU 和 GPU 的计算方式。
基本上,小版本号主要用于 **明确当前软件的执行时的各项前提要求**。从系统设备资源的使用到接口依赖的需求。
多端对接时,接口的调用需按照接口对应的版本来设计。

## 大版本号的使用
当软件发生了 **大幅改动**,尤其是发生了重构,许多 **旧代码被删除**,然后使用 **新的技术**或更换了 **开发策略**进行开发,或整个软件与其配套服务 **逻辑框架**发生变化时,软件发生大版本号的改动。
- **典型例子1**:Java 编程语言为了提升其功能和性能,方便性和安全性,**对系统库和语法进行了改变**,在此时 Java 语言的版本号进行 **大版本号升级**。使用 Java8 进行开发,和使用 Java9 进行开发的时候,开发者可以明显察觉 Java8 与 Java9 的差异,并且部分旧的 Java8 写法不再能在 Java9 里面使用。大版本号的变化,可以让使用者/对接者能够明确察觉到 **软件使用策略**的大幅变化,并且会专门研读文档中描述 Java8 与 Java9 之间差异的部分,来更好地使用软件。
- **典型例子2**:Python 编程语言在发展过程中不断增加新特性、改善其书写舒适度和代码美观度,不断增加小版本号,比如 Python2.6 经过改进后变为 Python 2.7,增加了新的语法特性。但是, Python2 在发展的过程中,人们发现一个无论如何也无法逃避的事实:现有的语法设计本身对 Python 语言的发展有较大的限制,比如:在面向对象编程语言的思维逻辑下,Python2 的语法并不完全尊重面向对象编程的思维逻辑,语法中有相当大的一部分内容不符合面向对象编程。于是,Python 的开发者们 **重新设计了这款编程语言的语法逻辑**,Python3 就诞生了。它的 **定位并没有明显的改变**:Python 的定位是 **专用于偷懒的舒适的脚本语言**。但是它的使用方式发生了较大的变化:整套语法都重新设计了。Python2 的使用者需要重新学习新的 Python3 的语法后才能使用 Python3,而任何一个使用 CMD/shell/bash 的苦难者在想要改变现状的时候,会发现不论是 Python2 还是 Python3 从定位上来说都很符合他的需求,但是因为 Python3 的大版本号是3,Python2 的大版本号是2,将来的趋势一定是 Python3 会变得越来越好,并且 Python3 的语法设计更加合理,是较为完备的面向对象编程语言,而 Python2 则不再得到维护,淹没于历史的海洋。于是初学者会选择 Python3 来学习使用,并使 Python3 的社区得到更好的发展,最终使 Python 编程语言可以成功从 Python2 时代过渡到 Python3 时代。

不过一般情况下一个软件开发者也可以 **不使用大版本号**,而是直接 **开新坑**,建立新工程,重新取名字,并找来完全不同的另一套依赖库,以及人马,针对某个业务场景进行全面优化。

# 接口设计规范
接口设计与软件版本挂钩。接口决定开发者之间的协作方式。协同开发时,不同的开发者之间必须约定好接口。

## 1. 设计接口时:需尽量多考虑接口会被以什么样的方式使用

### 做好接口参数合理性检查
- 你的接口的使用者不一定是你的同事,他也可以是入侵进来的黑客/你的敌人/你的测试人员。
- 当你的接口被人不慎进行错误调用时,你的程序不会因此崩溃/发生任何非设计性的行为(术语:**Robust**, **鲁棒性**,或称 **肉棒性**)。

### 公私分明:做好隔离,避免调用者越过接口使用你的私有内部数据
- 你在进行你自己的内部开发的时候,你会有需要频繁改动的代码。这部分代码做成私有,禁止外部直接访问。
- 如果能隐藏私有的部分使其不可见,**是上策**。
- 比如 C 语言使用结构体 `struct` 来设计概念上的对象时,你可以用一个 `void* internal;` 成员指向你程序内部工作时需要用到的数据。
- 如果不能隐藏私有部分,做好标记,让使用者知道这部分的数据别碰。
- 比如 Python 可以在私有的函数和变量名字前面**添加下划线** `_`,使其在外部不能被直接使用。
- 比如 C++/C#/Java 可以使用 `private` 关键字。
- 不希望被外界改动的数据需做成 **只读**属性。
- C++ 没有 **只读**属性,因此 C++ 在这个地方通过设计 `GetXxxxx()` 这样的函数来获取数据。
- C 语言同上。合理设计 getter/setter
- Java 可以使用 `final` 关键字,但这会导致数据你在内部也无法改动。此时也靠使用 getter。

### 充分详细设计报错逻辑:用好异常机制或错误码机制,使你的调用者可以判断接口是否正常工作。
- C++/C#/Java 设计接口时,**一定要明确**你会抛出的 **所有的异常种类**,便于调用者根据不同的异常类型进行处理。
- C++ 的 `std::map` 或者 `std:unordered_map` 的 `at()` 方法在 Key 无效的时候并不会自动提示 Key 的值。需要做好封装,自己提示 Key 的值用于排错。
- 使用错误码方式设计接口时,应尽量提供 **错误描述字符串**,便于打日志排查错误。
- 命令行程序明确返回值、**正常内容输出到 STDOUT、报错内容输出到 STDERR**,程序正常执行时应返回 `0` 作为 ExitCode,参数错误时返回 `1`,程序遇到问题而报错退出时返回 `-1`,以便于你的程序可集成到 bash/Python 脚本等。
- 调用命令行程序时应当读取命令行程序输出的所有内容包括 **ExitCode、STDOUT 和 STDERR**。
- Java 调用命令行程序时应同时读取 `getInputStream()` 和 `getErrorStream()` 的数据。

### 编写文档:描述清楚你希望别人如何使用你的接口。
- 文档一定要包含 **接口的版本**,针对不同版本的接口需要编写 **不同的文档**。
- 具体描述接口参数如何传递、如何使用返回值、什么样的参数有效、什么样的参数会导致错误。
- 具体描述接口内可能发生的功能实现行为,尤其是以下几点:
- 能否多线程调用,是否需要调用者自己上锁。
- 你内部是否使用了锁。
- 你大致是如何实现你的功能的。让调用者心里有个数。注意不能明确提到接口内部 **私有部分**的使用。
- 抛出异常的原因,以及所有的异常种类。
- 如何排错,以及排错案例。

### 编写 Demo 例程,让调用者可以 CV 你的代码来调用你的接口。
Demo 例程也应当包含充分的文档,去描述为何接口要如此调用。

## 2. 发布接口前三思:需尽量全面考虑其是否会影响后续的开发
接口一旦明确下来,对接的开发者们各自将针对接口进行自己的软件开发。接口功能提供者提供具体的功能实现,而接口功能的调用者则按照 **接口文档**对接口进行调用。

### 适当确保灵活性,留好所有可能需要的参数的入口
一定要留好所有的坑点,避免常量、字段数值写死,并保留余地使 **接口在不发生变化时**你可以给你的程序 **添加新的功能**或者除错。
发布前,尽量和对接的开发者进行协商,了解对方在使用时 **可能的需求。留好你的坑位**,在这些需求真正到来时,你可以减少对接口的改动。
注:灵活性不能太高,一旦高于接口对调用者的约束,你的接口设计将 **形同虚设。

## 3. 尽量避免对接口的设计进行改变
不到万不得已,不应改变现有接口的功能。 **可以增加新的接口,但是不能减少旧的接口**。
- 如果某接口函数的内部实现已经是不需要的了,则你依然无论如何都要提供这个函数,并使其内部没有任何行为,以确保你的接口符合调用者所理解的逻辑。
一旦发生接口变动,需要和所有对接者进行及时的沟通,并及时 **更新接口文档**以便于对接者查阅。
接口变动影响软件对接时,应当更新软件的 **小版本号**以体现 **软件运行需求**的明显变化。

云宝黛锡 发表于 2024-1-24 23:07:21

设计接口时,有没有可能量化地描述下列三个要求之间的权衡?
1. 减少通信压力;
2. 减少后台计算压力,尽量将一些校验留给前端;
3. 可靠性;

比如单词调用发送的数据量每降低1个单位,计算压力也会提高1个单位;然后可靠性每提高1个单位,计算压力也会提高1个单位,然后想办法求一个帕累托最优。

0xAA55 发表于 2024-2-8 00:50:47

云宝黛锡 发表于 2024-1-24 23:07
设计接口时,有没有可能量化地描述下列三个要求之间的权衡?
1. 减少通信压力;
2. 减少后台计算压力,尽量 ...

1. 可以考虑省去不必要的通信,但是必要的通信是无法省去的。
2. 出于信息安全考虑,前端过来的数据一律视为不可信任。
3. 请你具体描述一下“可靠性”。
页: [1]
查看完整版本: 版本管理与接口设计规范