在Unix的設(shè)計(jì)哲學(xué)中,do one thing 被廣大軟件設(shè)計(jì)開(kāi)發(fā)人員奉為圭臬,很多底層的基礎(chǔ)代碼只需要做成庫(kù),就可一勞永逸重復(fù)使用。但由于軟件的升級(jí),很多采用了包發(fā)布的方式,雖然方便了開(kāi)發(fā)者免受“晨后綜合癥”的困擾,卻也帶來(lái)了依賴地獄這個(gè)問(wèn)題。本文試圖闡述庫(kù)開(kāi)發(fā)過(guò)程中的問(wèn)題以及應(yīng)對(duì)事項(xiàng)。
菱形依賴
菱形依賴(diamond denpendency)是指當(dāng)主模塊所依賴的兩個(gè)庫(kù)引用了同一庫(kù)的不同版本的情況。如圖所示:

A模塊使用了B和C,但B依賴D的version-2,C依賴D的version-1. 這種情況下,很多依賴管理工具就會(huì)直接(或提示你)將D的版本統(tǒng)一到version-2鏈接到最終的可執(zhí)行文件中。但實(shí)際上,這是存在風(fēng)險(xiǎn)的。
分散式編譯:lib.a的頭文件陷阱
為了簡(jiǎn)化表達(dá),我們將模塊B省略,直接讓A依賴D的version-2,并在最終的仲裁機(jī)制中選擇了D的version-2. C原本依賴D的version-1,現(xiàn)在改為依賴version-2. 如圖。

這樣會(huì)有什么問(wèn)題呢?
C/C++編譯步驟
我們知道,C/C++的編譯需要4步:

第一步預(yù)處理則是針對(duì)頭文件中的#define 或者 #include等。假設(shè)D中某個(gè)常量,比如SHOW_ME_THE_MONEY 從100升級(jí)到1000. 那么libc.a中所使用的就是100,但A中被編譯進(jìn)去的則是1000. 原理其實(shí)很簡(jiǎn)單,正是因?yàn)榫幾g發(fā)生在兩個(gè)時(shí)間,僅僅通過(guò)發(fā)布頭文件和.a文件的方式,導(dǎo)致時(shí)間和空間的耦合。
如果不相信,我們稍后見(jiàn)代碼
鏈接的困惑
更難以接受的是某些功能不兼容的修改。比如下圖這種情況:

本來(lái)X期望A庫(kù)中的
f是return 0的實(shí)現(xiàn),但依賴仲裁將其升級(jí)到return 1。干說(shuō)了這么多,你可能不相信,我們上代碼:
/*base.h version-1*/
#ifndef BASE_H
#define BASE_H
const int SHOW_ME_THE_MONEY = 100;
#define I_AM_BLIND "But not d"
struct Base{
int id;
Base(int id):id(id){}
Base(const Base & other):id(other.id){}
void foo();
};
#endif
/*base.cpp version-1*/
#include <iostream>
#include "base.h"
void Base::foo(){
std::cout << "old base, id:" << id << std::endl;
}
這里的base相當(dāng)與我們最底層的庫(kù),它在一次升級(jí)中增加了我的MONEY和類中的一個(gè)flag。
/*base.h, upgrade to version-2, more money, add a flag member*/
#ifndef BASE_H
#define BASE_H
const int SHOW_ME_THE_MONEY = 1000;
#define I_AM_BLIND "But not deaf"
struct Base{
int id;
bool flag;
Base(int id, bool flag):id(id),flag(flag){}
Base(const Base & other):id(other.id),flag(other.flag){}
void foo();
};
#endif
/***new version: base.cpp****/
#include <iostream>
#include "base.h"
void Base::foo(){
std::cout << "i'm new foo, id:" << id << " flag: " << flag << std::endl;
flag = false;
}
我們的另外一個(gè)依賴庫(kù)x正在依賴version-1的base:
/*libx.h*/
#ifndef LIBX_H
#define LIBX_H
#include "base.h"
void call_libx();
#endif
----------------cpp below------------
/*libx.cpp : remeber , base version-1 in use..*/
#include <iostream>
#include "base.h"
void call_libx(){
std::cout << "my money in lib: "
<< SHOW_ME_THE_MONEY
<< " DH: " << I_AM_BLIND
<< std::endl;
Base b(110);
b.foo();
}
接下來(lái)是我們的主模塊,他同時(shí)依賴x和base:
#include <iostream>
#include "base.h"
#include "libx.h"
int main(int argc, char* argv[]){
std::cout << "my money in app:"
<< SHOW_ME_THE_MONEY
<< " DH:" << I_AM_BLIND
<< std::endl;
call_libx();
return 0;
}
我們將新舊base.h,base.cpp都編譯成libbase.a,然后將libx也編譯成.a,供主模塊編譯鏈接,運(yùn)行后的結(jié)果和前文所說(shuō)一致:

常量在不同的模塊有不同的值,flag字段則出現(xiàn)隨機(jī)值。(這很危險(xiǎn)哦)
如何應(yīng)對(duì)
一般來(lái)講,如果你只負(fù)責(zé)上圖中A模塊或者APP的代碼,并無(wú)庫(kù)的權(quán)限,也許只能向庫(kù)作者提交ISSUE單了。不過(guò)假設(shè)我們負(fù)責(zé)整個(gè)架構(gòu)的代碼,如何應(yīng)對(duì)呢?
源碼依賴
對(duì)于分散式編譯的問(wèn)題,只要在我們的代碼中廢棄.a這種方式,就能將編譯時(shí)刻統(tǒng)一到當(dāng)前。
后向兼容和末端依賴
對(duì)于庫(kù)開(kāi)發(fā)者,除非歷史包袱特別的重,應(yīng)后向兼容所有版本號(hào)小于自己的版本。類似Python,最新的2.7可以運(yùn)行2.5或者2.6的代碼.
而對(duì)應(yīng)使用者,則應(yīng)永遠(yuǎn)依賴最新的發(fā)布。在依賴管理工具上,提供依賴最新的這種抽象依賴手段。
更新
git ci --amend -m "thanks to 微笑的魚Lilian"
謝謝微笑的魚Lilian, 提出了一個(gè)為什么libx中使用了Base(int)的構(gòu)造函數(shù)仍然可以鏈接通過(guò)的問(wèn)題。
原因是第一個(gè)版本的base.h的構(gòu)造函數(shù)是inline在頭文件中的,當(dāng)編譯libx.cpp的時(shí)候,#include將其展開(kāi)在了libx.cpp中導(dǎo)致的。
這讓我警醒到,原來(lái)不同版本的inline函數(shù),和常量、宏一樣,容易發(fā)生分散式編譯的問(wèn)題。