依賴管理


在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ù)的不同版本的情況。如圖所示:

diamond dependency.png

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. 如圖。

castrate-B.png

這樣會(huì)有什么問(wèn)題呢?

C/C++編譯步驟

我們知道,C/C++的編譯需要4步:

compiling process.png

第一步預(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)代碼

鏈接的困惑

更難以接受的是某些功能不兼容的修改。比如下圖這種情況:

link_compatible.png

本來(lái)X期望A庫(kù)中的freturn 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ō)一致:

result.png

常量在不同的模塊有不同的值,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)題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容