C++ API 中的版本控制
目录
1 应该提供 API 版本号信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//version.h #include <string> #define API_MAJOR 1 #define API_MINOR 2 #define API_PATCH 0 class version { public: static int GetMajor(); static int GetMinor(); static int GetPatch(); static std::string GetVersion(); static bool IsAtLeast(int major,int minor,int patch); static bool HasFeature(const std::string& name); } |
这个类提供了单独返回当前版本号的主、次、补丁版本号的的访问函数。它们返回各 define 定义的值。GetVersion 方法向用户返回友好的字符串版本号信息。 当用户相比较版本号时,他们通常不关心版本号本身,而是想知道某些特性在该版本的API中是否存在。HasFeature() 方法 不是告诉用户哪个版本的 API 引入了哪些特性,而是让亿们直接测试某个特性是否可用。
2 软件分支策略
一般的大型项目通常会涉及到某种形式的分支策略,这需要同步开发、维护不同的软件版本发布。我们将讨论为项目选择分支策略和方针时需要考虑的一些事项。
2.1 分支策略
每个软件都需要一条 trunk (主干)代码路线,它是软件项目源码
的持久库。对于对于每次版本的发布
,可以从主干进行。每次新版本的开发,可以从主干的代码添加分支,而使主干的代码不受开发的影响而保持稳定。右图是一种常见的分支策略。
2.2 API与并行分支
在API发布以后,对其所有的更改都应该表现为一个连续的过程,即不发布不兼容的非线性版本的API,一个版本的API应该是前一个版本功能的严格超集。在大型项目中,通常会有几条并行的代码分支在进行同时开发,这就会产生若干个并行维护的API版本。因此,工作在不同并行分支上的团队互不引入不兼容的特性是非常重要的。下列方法可以帮忙处理这种潜在的问题:
- 制定开发分支的目标: 项目通常有开发分支和发布分支。只要保证不直接在发布分支上进行个性,就能减少由于未将当前版本的修改合并到主干而导致API在下一个版本丢失的可能性。如果需要发布分支上修改的API,那么应该先将它提交到主干,并进行合并。这适用于发布分支的修改。
- 经常合并到主干中。 对于API的任何修改,要么在主干上进行,要么尽早合并到主干中。
- 审查过程。独立的API审查委员会应该在公有API发布之前对其进行监督和审查,以确保API不存在冲突或无法向后兼容的修改。
这些解决方案试图将API的准确性保持在主干代码中,而非将各个修改散落在多个分支中。
3 API的生命周期
API的维护不同于一般的软件产品。这是因为API开发具有额外的约束力:不能破坏已有客户的程序。如果API进行修改,可能会破坏已有的客户的程序。API是一种有契约:你必须确保遵守你制定的规则。 上图给出了一个典型的API生命周期简图。在其生命周期内,最听重要的事件是初始发布。在这个关键点之前,对设计和接口作出的重大修改都是可以接受的。但是在初始发布后,一旦用户使用了你的API进行开发,就需要承诺提供向后兼容性,你能够修改的范围受到了限制。 API开发有4个常见的阶段。
- 发布前:初始发布前,API可以遵循标准的软件开发周期,包括需求收集、计划、设计、实现、测试等。实际上还可以向用户发布API早期版本以获得反馈和建议。这时的版本号可以使用 0.x ,以表明API仍处于活跃地开发中,在正式版本发布前可能会有大的修改。
- 维护:API发布后仍然可以修改,但是为了维护向后兼容性,只能增加新的方法或类,以及修复已有方法实现中的错误。为确保修改不破坏兼容性,好的方式是在新版本发布前进行回归测试和API审查。
- 完成:在某个时间点,可以认定API已经成熟,不应对接口做进一步的修改。这个阶段,API的稳定性是最重要的,通常只会修复某些错误。
- 弃用:有些API最终会达到其生命周期的终点,此时它们会被放弃使用。当API不在提供应有的服务,或有新的、不兼容的API取代原的的API,原来的API会被弃用。弃用的API不应在任何新的开发中使用,已有的客户程序也应该放弃使用这些API.
4 如何维护向后兼容性
4.1 添加功能
一般来说,添加新的类、接口不会对已有的API进行改变,不会破坏已有的代码。这对于API兼容性来说是好的。例外的是,给抽象基类添加新的纯虚成员函数是不向后兼容的。
1 2 3 4 5 6 7 |
class Demo { public: virtual ~Demo(); virtual void ExistingFun() = 0; virtual void NewFun() = 0; //新版本的添加的API }; |
客户所有的派生类都必须定义这个新方法的实现,否则它们就不能被实例化。变通的方法是为添加到这个抽象类中的新方法提供一个默认实现,即把其定义为虚方法而不是纯虚方法。
1 2 3 4 5 6 7 |
class Demo { public: virtual ~Demo(); virtual void ExistingFun() = 0; virtual void NewFun(); //新版本的添加的API }; |
4.2 修改功能
当我们需要修改一个API,有很多技巧可以提高API的兼容性:
4.2.1 可选参数与返回值
为方法添加可选参数是向后兼容的:
1 2 3 4 5 |
// V1.0 void SetImage(Image* img); // V1.1 void SetImage(Image* img, bool bKeepFormat = true); |
同样,修改原本不需要验证的返回值也是向后兼容的:
1 2 3 4 5 |
// V1.0 void SetImage(Image* img); // V1.1 bool SetImage(Image* img); |
4.2.2 添加名称类似的函数
这种方法在 C 风格API中比较常见。在API中引入一个名字不同的函数,同时重构旧的方法的实现,使之调用新的方法。
1 2 3 4 5 6 7 |
// V1.0 HWND CreateWindow(); void* OpenFile() // V1.1 HWND CreateWindowsEx(); //添加新的API void* CreateFile(); //添加新的API |
4.2.3 修改功能而不修改方法签名
这种情况一般为了修复API中的错误。也可以在API的实现中使用外部变量来控制API的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// V1.0 class Demo { public: Demo(){}; void Fun(int nVal) { //do thing A } }; // V1.1 class Demo { public: Demo():m_bFlag(false){}; void SetDoSth(bool bFlag) { m_bFlag = bFlag; } void Fun(int nVal) { if(m_bFlag) { //do thing A } else { //do thing B } } private: bool m_bFlag; }; |
上面这段代码中,V1.1的API和V1.0的逻辑是一样的。如果客户需要使用修改后的功能(do thing B),则可以对使用 SetDoSth() 方法来进行逻辑控制。
4.3 弃用功能
弃用功能一般是指建议用户不要使用某API.为维护兼容性,该API仍然可以使用,但是会以某种方式向用户发出警告,使用户有时间来处理其代码。例如在 MSVC 中,使用 fopen() 会得到警告,提示用户使用更安全的 fopen_s() . 当准备弃用API时,需要在API中说明弃用它的理由,并提供解决方案,如使用某API进行替代。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#ifdef __GNUC__ #define DEPRECATED __attribute__((deprecated)) #elif defined(__MSC_VER) #define DEPERCATED __declspec(deprecated) #else #define DEPRECATED #pragma message("DEPRECATED is NOT defined for this compiler") #endif // __GNUC__ class Demo { public: DEPRECATED std::string GetName(); std::string GetFullName(); } |
当用户调用 GetName() 方法时,编译器会输出警告消息,告诉用户该方法已经弃用。