---
導語
糟糕的物理設(shè)計是對遺留大型系統(tǒng)中進行重構(gòu)的非常棘手的一個問題,本文相機闡述了遺留系統(tǒng)中存在哪些糟糕的物理設(shè)計,它們對重構(gòu)所帶來的哪些惡略影響,以及我們在重構(gòu)過程中應(yīng)該如何處理這些問題。文中后面還介紹了關(guān)于物理設(shè)計的一些工具,其中包括本人開發(fā)的自動化頭文件拆分工具。
1.物理設(shè)計VS邏輯設(shè)計
物理設(shè)計
物理設(shè)計主要是軟件設(shè)計中的物理實體(文件)的設(shè)計,例如某個函數(shù)定義應(yīng)該放在哪個文件中、某個函數(shù)是否需要Inline等,從物理設(shè)計看到的是系統(tǒng)中的大量文件實體。邏輯設(shè)計
邏輯設(shè)計主要針對軟件設(shè)計中的邏輯實體關(guān)系的設(shè)計,例如類之間的關(guān)系,Has a/use a/is a的關(guān)系,從邏輯設(shè)計看到的是大量的邏輯實體,如類,函數(shù),結(jié)構(gòu)體。
物理設(shè)計的主要目標是減少文件的物理依賴,而邏輯設(shè)計的主要目標是減少邏輯依賴。物理依賴更多的體現(xiàn)為編譯時的依賴和鏈接時的依賴,物理依賴受邏輯依賴影響,但是又不局限于邏輯依賴,在一個大型軟件系統(tǒng)中,物理設(shè)計和邏輯設(shè)計是完全不同的范疇,也是一個需要重點關(guān)注和考慮的問題。很多人認為物理設(shè)計主要考慮頭文件的設(shè)計,其實是非常錯誤的,正確的物理設(shè)計不僅要考慮頭文件的設(shè)計,還要考慮源文件的設(shè)計,從而達到編譯單元的物理依賴設(shè)計。
2.糟糕的物理設(shè)計有哪些
2.1 巨型文件
我們這里談?wù)摼扌臀募⒉粏沃妇扌皖^文件,就像物理設(shè)計并不是單指頭文件的物理設(shè)計一樣。巨型源文件和頭文件一樣也是一種非常糟糕的物理設(shè)計。在遺留的大型系統(tǒng)中,巨型頭文件和巨型源文件隨處可見,正是因為這種糟糕的物理設(shè)計導致我們的系統(tǒng)中代碼構(gòu)成一個巨大的網(wǎng)狀物理依賴系統(tǒng),如下圖所示
2.2 糟糕的文件封裝
這里并不是談?wù)揅++的域名空間概念,為了清楚的說明文件封裝的概念,我們先介紹如下幾個概念。
- 聲明
- 定義
- 編譯單元
- 內(nèi)部鏈接
- 外部鏈接
一個聲明將一個名稱引入一個程序,而一個定義提供了一個實體,在程序中唯一描述。編譯單元通常指編譯過程中編譯器看到的一個單位,在C/C++中,通常是以每個源文件為一個編譯單元。如果一個名字對于他的編譯單元是局部的,并且連接時與其他編譯單元中定義的標示符名稱不沖突,那么這個名字就是內(nèi)部鏈接的。如果一個名字有外部鏈接,會產(chǎn)生外部符號,那么在多文件系統(tǒng)編譯鏈接過程中,這個名字可以和其他編譯單元交互。
結(jié)構(gòu)體定義具有內(nèi)部鏈接性,所以在每個需要使用當前結(jié)構(gòu)體定義的編譯單元都需要顯示包含結(jié)構(gòu)體定義的頭文件,本質(zhì)上每個編譯單元中都有一個完整的結(jié)構(gòu)體定義。同樣C+ +中的類定義也是具有內(nèi)部鏈接性,每個使用了類定義的編譯單元都需要包含類定義的頭文件,但是類定義中的非內(nèi)聯(lián)函數(shù)屬于函數(shù)聲明,所以類的非內(nèi)聯(lián)方法定義會產(chǎn)生外部符號,而類的內(nèi)存布局定義并不會產(chǎn)生外部符號。根據(jù)以上分析可以發(fā)現(xiàn),類定義本質(zhì)上比較像結(jié)構(gòu)體定義+ 函數(shù)方法的聲明。我們在平時的C++項目中,經(jīng)常看到的鏈接錯誤看到的經(jīng)常是找不到某個類方法的定義而不是找不到某個類的定義,因為找不到類定義屬于編譯錯誤。
在討論清楚這些之后,我們再看一下C語言系統(tǒng)中哪些具有內(nèi)部鏈接,哪些具有外部鏈接:
1)內(nèi)部鏈接:結(jié)構(gòu)體定義,宏定義,typedef, enum, union
2)外部鏈接:全局變量定義,全局函數(shù)定義。
我們提倡內(nèi)部鏈接的東西盡量放到源文件中,同時盡量減少定義具有外部鏈接名字,然而在遺留系統(tǒng)中經(jīng)常并關(guān)注這些概念,從而導致系統(tǒng)中存在大量的這些問題。
- 全局變量隨處可見
- 全局函數(shù)不加管制
- 宏使用泛濫
- 全局定義隨處可見
2.3 巨型接口頭文件
巨型接口頭文件從嚴格意識上將并不會引起系統(tǒng)內(nèi)各個模塊的物理依賴,但是它也是一種非常糟糕的物理設(shè)計。
在我們的業(yè)務(wù)代碼中,在出現(xiàn)下面幾種現(xiàn)象時都需要包含一整個公共接口頭文件。
- 如果只使用了公共頭文件接口中的一個結(jié)構(gòu)體,我們需要包含這個頭文件。
- 如果只使用了公共頭文件接口中的一個宏定義,我們需要包含這個頭文件。
- 當包含的某個公共頭文件編譯又依賴于另外的公共接口頭文件,那么我們還需要包含依賴的相應(yīng)頭文件。
- 公共接口頭文件的結(jié)構(gòu)體定義限制了前置聲明,導致我們只使用公共頭文件中某個結(jié)構(gòu)體的指針或者引用的情況下也需要包含整個頭文件。
前面幾條規(guī)則很好理解,這里單獨解釋一下第四條。當我們在重構(gòu)過程新增加的代碼中,如果只使用某個結(jié)構(gòu)體的指針或者引用的時候,通常情況下只需要前置聲明即可,并不需要包含相應(yīng)的頭文件,這是C++減少物理依賴的一個非常重要的手段。但是現(xiàn)有的公共頭文件中的結(jié)構(gòu)體定義形式有下面兩種:
typedef struct
{
WORD32 dwValue;
}T_StructName1;
typedef struct tagStructName2
{
WORD32 dwValue;
}T_StructName2;
顯然上面這兩種結(jié)構(gòu)體定義是C語言的遺產(chǎn),不能很好的支持前置聲明。第一種方式T_StrictName1屬于typedef重定義的名字,是不支持前置聲明的。第二種方式僅支持tagStructName2的前置申明,但是與真實使用名字T_StructName2不一致,同樣編譯器會報錯。所以下面給出的這種方式是很好的兼容老式的C語言和C+ +的一種方式,具體定義形式如下:
typedef struct StructName2
{
WORD32 dwValue;
}StructName2;
3.糟糕的物理設(shè)計的影響
3.1 復用已有功能模塊困難
一般情況下,我們談復用的時候,的確是希望復用一些軟件中的一些邏輯實體,看似和物理設(shè)計無關(guān),其實不然。真實的復用肯定要承載一定的物理實體上,如果我們期望復用某一個邏輯實體,需要把承載相應(yīng)的邏輯實體的相關(guān)物理文件編譯鏈接進來。
如下圖所示,雖然functionA()與functionB(), functionC()在邏輯上沒有任何關(guān)系,但是由于糟糕的物理設(shè)計,我們想復用functionA(),就必須把兩個編譯單元fileA.C和fileB.c兩個編譯單元作為一個整體才能夠被復用。
//fileA.c
void functionA()
{
printf("hello world\n");
}
int functionB()
{
return functionC();
}
//fileB.c
int functionC()
{
return 10;
}
在理想狀態(tài)下,我們期望我們系統(tǒng)中的開發(fā)的功能和特性每一個塊都是可以獨立復用的,而不是所有的功能和特性作為一個整體才可以復用,如下圖所示,左側(cè)系統(tǒng)的可復用性是優(yōu)于右側(cè)的。但是針對遺留系統(tǒng)而言,由于糟糕的物理設(shè)計導致系統(tǒng)各個編譯單元之間經(jīng)常是右側(cè)網(wǎng)狀依賴,從而導致了系統(tǒng)所有功能實體必須做為一個統(tǒng)一的整體才能被復用。
而我們在對大型遺留系統(tǒng)進行重構(gòu)的過程中,并不是剛開始就對整個系統(tǒng)進行重構(gòu),而是選擇其中的一部分模塊進行重構(gòu),那么就需要復用其他模塊,而遺留系統(tǒng)中網(wǎng)狀的物理依賴關(guān)系導致我們想復用已有系統(tǒng)的每個模塊都非常困難。
3.2 系統(tǒng)難以理解
軟件功能自從誕生依賴,可理解性都一直扮演著非常重要的角色,自從簡單設(shè)計四原則被提出來之后,大家對可理解性有更深一步的認識,但是遺留系統(tǒng)的可理解性通常都非常差。
可理解性不等于注釋,遺留系統(tǒng)中經(jīng)常增加了很多注釋,但是這些注釋對可理解性方面收效甚微。相反,遺留系統(tǒng)中隨處可見的全局變量,不加控制的全局方法,導致我們在閱讀和理解代碼的過程中很難搞清楚每個模塊真正對外提供的接口是什么,同時也就非常難理解模塊真正干了哪些事情。
3.3 構(gòu)建測試用例困難
易復用的東西通常是易測試的,而遺留系統(tǒng)糟糕的物理設(shè)計導致可測試性極差。相對而言針對遺留系統(tǒng)而言,構(gòu)造系統(tǒng)級別的FT測試會容易一些,但是FT測試在前期有較大收益但是存在一定的局限性。通常FT測試可以覆蓋開發(fā)系統(tǒng)的大部分功能,但是系統(tǒng)中還存在一些功能使用FT測試非常困難同時成本也是非常大。所以我們需要構(gòu)建UT, FT, SAT等一整套測試體系,那么糟糕的物理設(shè)計對構(gòu)造UT測試來說簡直就是噩夢。
通常構(gòu)造UT級別測試的過程中,需要做的事情有一下幾個方面:
- 構(gòu)造測試輸入輸出
- 依賴邊界打樁,
- 借用mockcpp幫助測試
通常情況下我們可以針對系統(tǒng)中每個可復用單元來構(gòu)造UT測試,如果系統(tǒng)可復用單元粒度比較小,那么測試構(gòu)造就會非常容易,UT測試的編譯和開發(fā)都會比較小,那么使用Testngpp測試框架就可以非常容易的構(gòu)造UT用例。如果系統(tǒng)中的可復用單元比較大,為了構(gòu)造測試用例,我們需要構(gòu)造的輸入輸出上下文成本就會變大,經(jīng)常就不得不使用mockcpp或者自己構(gòu)造的樁函數(shù),然后糟糕的物理設(shè)計,會顯著增加我們在這方面的成本。
3.4 編譯時間過長
在大型遺留系統(tǒng)重構(gòu)項目中,為了消除重復和達到更好的可理解性,我們更期望去開發(fā)一些更小的,且具有單一職責的類。這個時候,一個奇怪的問題出現(xiàn)了,隨著我們重構(gòu)代碼量越大,新開發(fā)的類越多,編譯時間越來越長!
仔細分析之后,發(fā)現(xiàn)又是巨型接口頭文件惹的禍。遺留系統(tǒng)中公共接口頭文件通常都非常大,有的甚至超過3000行。大家應(yīng)該都知道C++編譯期間,默認都是以每個cpp文件為一個編譯單元,然后把頭文件在cpp文件中的位置展開并進行編譯。所以我們在重構(gòu)過程中增加的一些很小的cpp文件,看似非常小,但是真實編譯的時候如果包含了接口頭文件,那么編譯起來也很長。
編譯時間長會嚴重的影響重構(gòu)的節(jié)奏,使得重構(gòu)變得非常困難。很多人可能會說,我們可以使用并行編譯呀,make的時候加一個-j
就搞定了呀!那我告訴你沒有最快,只有更快。對于期望編譯時間可以到達秒級的程序員來說,編譯時間沒有上限。
還有人可能會說,我們可以用聯(lián)合編譯呀,把多個cpp文件打包成一個大文件來進行編譯呀,我不得不承認這個方法的確可以在很大的程度上改善巨型頭文件引入的編譯時間過長問題,但是我這里必須鄭重的提醒你一下,我們一定要慎用聯(lián)合編譯,因為聯(lián)合編譯破壞了文件封裝性,導致原來文件中的static 定義,匿名namespace的就變得不再是本文件內(nèi)可見,所以一定要慎用。
4.重構(gòu)中如何應(yīng)對糟糕的物理設(shè)計
4.1將物理依賴層次化
在遺留系統(tǒng)中,循環(huán)物理依賴隨處可見,很大程度上影響了可理解性和可復用性,然而針對C語言的遺留系統(tǒng)中,消除循環(huán)物理依賴可以通過層次化物理依賴來解決。
//AB.c
void function A()
{
functionB();
functionD();
}
void function B()
{
}
//CD.c
void function C()
{
functionB();
functionD();
}
void function D()
{
}
如上代碼所示,編譯單元AB和編譯單元BD之間存在循環(huán)依賴的情況,可以通過拆分分層把B和D拆分到單獨的編譯單元中,來消除互相循環(huán)依賴的情況,如下圖所示,將不再存在循環(huán)依賴的情況。
經(jīng)常調(diào)整物理設(shè)計和物理依賴,可以把原有系統(tǒng)的網(wǎng)狀物理依賴可以調(diào)整為分層的物理依賴,如下所示:
分層的物理依賴主要原則是,每一個層的編譯單元只物理依賴于它下層的編譯單元。當系統(tǒng)的物理設(shè)計滿足分層架構(gòu)的情況下,不僅非常有利于增量式測試,也有利于代碼復用和重構(gòu)。如上圖所示,每個編譯單元都可以與它下層依賴的編譯單元組合起來為一個可復用單元,那么我們就可以針對每個可復用單元設(shè)計包圍測試并進行重構(gòu)了。
4.2提升文件封裝性
上面小節(jié)主要談?wù)撐锢硪蕾嚕嗟恼務(wù)撌蔷幾g單元之間的物理依賴,而這里討論的文件封裝性是另外一個層面,主要是針對介紹提升文件的封裝性,減少對外部鏈接域的污染,減少對全局名字域的污染,同時提升已有功能模塊的可理解性。
具體措施有如下一些:
- 消滅遺留C系統(tǒng)中的extern 關(guān)鍵字。
- 可以內(nèi)部鏈接的方法都需要static
- 結(jié)構(gòu)體定義盡量的移入到源文件中
舉一個簡單的例子,代碼重構(gòu)前:
//oldModule.h
typedef struct TYPE_A
{
int a;
int b;
}TYPE_A
typedef struct TYPE_B
{
int c;
char d;
}TYPE_B
extern int g_openswitch;
void funA(TYPE* A);
int funB(TYPE* B);
//oldModule.c
#include "oldModule.h"
int g_openswitch;
int funB(TYPE* B)
{
return b.c* b.d;
}
void funA(TYPE* A)
{
TYPE_B b;
b.c =3;
b.d = '4';
a. b = funB(&b);
}
重構(gòu)后
//oldModule.h
typedef struct TYPE_A
{
int a;
int b;
}TYPE_A
bool isSwitchOn();
void funA(TYPE* A);
//oldModule.c
#include "oldModule.h"
static int g_openswitch;
bool isSwitchOn()
{
return g_openswitch == 1;
}
static int funB(TYPE* B)
{
return b.c* b.d;
}
void funA(TYPE* A)
{
TYPE_B b;
b.c =3;
b.d = '4';
a. b = funB(&b);
}
如上所示,C語言并不規(guī)定所有的結(jié)構(gòu)體定義必須要放到頭文件中,也不是所有的函數(shù)都需要聲明。為了體現(xiàn)更好的實現(xiàn)封裝性,我們需要把不需要對外暴露的結(jié)構(gòu)體放到源文件中,把不需要對外暴露的接口static到源文件中,同時消除全局變量。這樣頭文件可以看做對外暴露較少的外部鏈接接口,只應(yīng)該看到必要的結(jié)構(gòu)體定義和公共函數(shù)接口聲明。
我們在對遺留系統(tǒng)中某個編譯單元或者模塊進行重構(gòu)的過程中,首先需要理解原有系統(tǒng)。要理解原有系統(tǒng),第一步必須要清楚系統(tǒng)中的每個模塊哪些是對外公共的接口,哪些是內(nèi)部實現(xiàn)。然后我們才可以針對外部公共接口構(gòu)造包圍測試,重構(gòu)相應(yīng)的編譯單元或者模塊。
4.3 消滅巨型接口頭文件
在對遺留系統(tǒng)進行重構(gòu)的過程與開發(fā)新的系統(tǒng)有很多的差異,因為遺留系統(tǒng)對重構(gòu)增加了很多內(nèi)在的約束,如下所示:
- 原有子系統(tǒng)的公共接口頭文件通常我們并沒有所有權(quán),那就意味著我們不能做任何修改。
- 重構(gòu)團隊經(jīng)常需要與原有團隊同步前進,重構(gòu)團隊需要不斷同步適應(yīng)公共接口頭文件的變更。
優(yōu)秀的公共接口頭文件應(yīng)該滿足如下要求:
- 單獨可以編譯通過(自滿足)。
- 每個接口頭文件中應(yīng)該只包含單個結(jié)構(gòu)體定義。
- 接口中的結(jié)構(gòu)體定義支持前置聲明。
- 公共接口頭文件中只包含必要的頭文件。
其他條目很好理解,這里單獨解釋一下第二條:”每個接口頭文件應(yīng)該只包含單個結(jié)構(gòu)體的定義“,這個應(yīng)該完全是我自己提出的概念,存在少許爭議。我們平時提到的接口隔離原則,要求針對不同的客戶定義獨立的接口,從而不讓客戶看到它不關(guān)心的接口,但并沒有嚴格要求到每個接口頭文件只包含單個結(jié)構(gòu)體定義。我提出這個規(guī)則是基于這樣的一個前提,如果重構(gòu)項目中類的職責很單一,大部分都是很小的類,那么通常情況下只使用某一個結(jié)構(gòu)體是常態(tài),如果在一些特殊情況下,需要使用多個結(jié)構(gòu)體,那么包含多個頭文件也并無大礙。
為了解決遺留巨型接口頭文件對我們重構(gòu)的制約,最容易想到的方式就是新增加頭文件,然后把結(jié)構(gòu)體按照我們的期望的格式添加到里面,然后在我們重構(gòu)的新代碼中,使用新增加的頭文件即可。這時我們碰到了三個新問題,第一個問題就是,我們破壞了結(jié)構(gòu)體的dry原則,我們新增加的頭文件中的結(jié)構(gòu)體與原來公共頭文件的結(jié)構(gòu)體本質(zhì)上是一個結(jié)構(gòu)體,但是卻用兩個定義。第二個問題,我們的公共接口頭文件中,一共有800多個結(jié)構(gòu)體定義,如果每個結(jié)構(gòu)體都拆分一個頭文件,那就需要拆分800次。我們也經(jīng)常發(fā)現(xiàn),當新開發(fā)一個小類的時候,拆分頭文件時間遠遠大于開發(fā)新代碼的時間。第三個問題,如果我們能夠一次搞定也就罷了,但是我們重構(gòu)版本經(jīng)常需要跟著大版本一起前行,如果大版本中的公共頭文件接口發(fā)生修改,我們怎么保證和內(nèi)部新增加的頭文件中的結(jié)構(gòu)體還是一致的,難道需要再把800個結(jié)構(gòu)體重新拆頭文件一次嗎?
為了解決這里問題,需要借助自動化工具,請參考5.2自動化頭文件拆分工具。
5物理設(shè)計相關(guān)工具集
5.1 biicode
biicode 是一個支持多平臺的 C/C++ 依賴管理器,可以很方便集成到 Visual Studio 和 Eclipse CDT 中,目前已經(jīng)開源了客戶端的代碼在github上面, 官方稱會逐漸開源全部代碼.
- 官方博客: blog.biicode.com/biicode-open-source-client/
- 項目主頁: biicode.github.io/biicode/
這個項目算是彌補了C/C++一直沒有一個像樣的包管理器的缺陷,當你代碼使用第三方庫的過程中,原則上你可能只使用庫中的一部分代碼實現(xiàn),但是我們通常是把這個庫文件完整的編譯進你的系統(tǒng)中。使用biicode之后,如果你只包含了庫中的某個頭文件,那么它只會把庫中和你相關(guān)的實現(xiàn)編譯到你的系統(tǒng)中。這個時候物理設(shè)計就扮演著非常重要的一個環(huán)節(jié),如果你系統(tǒng)有良好的物理設(shè)計,那么biicode就會發(fā)揮比較大的價值。
未完待續(xù)。
5.2自動化頭文件拆分工具
本章節(jié)主要介紹本人開發(fā)的自動化頭文件拆分工具的實現(xiàn),以及如何解決遺留系統(tǒng)中的巨型頭文件問題。
5.2.1消除預編譯宏
我們想做的第一件事情就是去除頭文件中的預編譯宏。如下面頭文件,我們重構(gòu)的時候只關(guān)注某個單板類型,但是在頭文件里面,我們卻看到了很多我們不關(guān)心的產(chǎn)品的結(jié)構(gòu)體定義;另外一方面結(jié)構(gòu)體中過多的預編譯宏也著實讓我們很難受,所以我們首先要消滅它。
typedef struct {
WORD16 wGid;
#if (_LOGIC_BOARD == _LOGIC_XXX_BOARD_1)
UCHAR ucSimultANAndSRS;
UCHAR aucRsv0[3];
#endif
#if (_LOGIC_BOARD == _LOGIC_XXX_BOARD_2)
UCHAR aucRsv2[12];
#endif
} T_PucchRbScheInfo;
剛開始我首先想到是使用ruby腳本去解析這些預編譯宏來判斷到底哪些代碼是我們關(guān)注的。但是我很快的發(fā)現(xiàn)頭文件中有很多很復雜的預編譯宏,而且有很多包含嵌套,我很認真的分析各種嵌套關(guān)系,最后終于在考慮了5層嵌套的情況下搞定了這些預編譯宏,同時也針對新寫腳本增加了測試用例。但是我還是對這些復雜的腳本代碼極度的不信任,這時一位團隊成員給了我一個很重要的建議,利用編譯器來做這些事情,它們更專業(yè)。
一語中的,那就讓編譯器來幫我來做吧。這里首先給大家介紹一個概念吧,那就是預編譯。我們的編譯器在編譯之前首先會做一次預編譯,預編譯工作主要包括頭文件原位置插入和宏替換,同時也消除了預編譯宏和注釋。再補充一點,我們團隊有一套比較強大的makefile,同時支持模塊級別的,子系統(tǒng)模塊集成級別,以及單板級別的make.當然也支持針對每個文件的預編譯了,那后面的事情就非常簡單了!
實現(xiàn)上面功能的主要代碼如下,由于編譯器的預處理只會對cpp文件進行預編譯,所以我們還要先對頭文件進行一些預處理,并轉(zhuǎn)換為cpp文件,構(gòu)造一個在命令行上的make命令,然后執(zhí)行make,具體ruby代碼如下所示。
def generate_make_cpp()
remove_old_file(@make_cpp_path)
make_cpp = File.open(@make_cpp_path, "w")
lines = File.open(@header_path,"r").readlines
lines.each do |line|
if ((line.include?"#include") || (line.include?"#define"))
next
end
make_cpp.puts line
end
make_cpp.close
end
def run_gcc()
gccCmd = @gcc + @make_cpp_path + " > " + @make_i_path
run_cmd(gccCmd)
end
大家可能發(fā)現(xiàn)我們我在頭文件轉(zhuǎn)換為cpp文件時候,把頭文件中的宏定義,還有包含頭文件的行都刪除了,仔細想想很容易就能得到答案。刪除#define主要是為了確保我們拆分的結(jié)構(gòu)體中的宏不會變成魔術(shù)數(shù)字,而刪除#include主要是為了不讓我們重復對一個結(jié)構(gòu)體進行重復的拆分。經(jīng)過預編譯處理之后的中間文件幫我們消除了預編譯宏,同時也消除了那些雜亂無章的注釋,代碼干凈漂亮如下所示,我們就可以基于這樣的代碼進行拆分了。
typedef struct {
WORD16 wGid;
UCHAR aucRsv2[12];
} T_PucchRbScheInfo;
5.2.2有效識別接口文件中的結(jié)構(gòu)體
要做到針對每個結(jié)構(gòu)體拆分單獨的頭文件,那么首先需要準確的識別結(jié)構(gòu)體或者枚舉,并生成拆分頭文件的文件名。這里大家可能會有疑惑,我們?yōu)槭裁礇]有把結(jié)構(gòu)體按照我們的期望的駝峰式進行重新命名,道理很簡單因為這些結(jié)構(gòu)體在以前遺留代碼中還在使用,我們并不想因為這點就去就修改遺留代碼。
要做到上面這點,我們需要在接口頭文件中識別一個結(jié)構(gòu)體的開始定義,結(jié)構(gòu)體的名字,還有結(jié)構(gòu)體定義的末尾。要做到這些也很容易,因為每個腳本語言都具有非常強大的正則表達式模式匹配功能,當然ruby也不例外。讓我們看看ruby是如何做到的。
def is_type_def_begin(line)
(line.include?"typedef")
end
def is_type_def_end(line)
/[}][\s]*[TE]/.match(line)
end
def get_struct_name(line)
struct2 = /[TE][\w]+/.match(line.force_encoding("gb2312"))
struct2[0]
end
同時我們還需要根據(jù)結(jié)構(gòu)體的名字,生成拆分的頭文件名字,同樣我也可以利用正則表達式。
def generate_header_file_name_from(structname)
header_file_name = structname.gsub(/[ET]_/,"ce_");
header_file_name = header_file_name.gsub(/[a-z][A-Z]/){|s| s[0] + '_' + s[1]}
header_file_name = header_file_name.gsub(/2/, "_to_")
header_file_name = header_file_name.gsub(/4/, "_for_")
header_file_name = header_file_name.gsub(/[A-Z][A-Z][a-z]/){|s| s[0]+'_'+s[1]+ s[2]}
header_file_name = header_file_name + ".h"
header_file_name.downcase
end
當生成頭文件名字之后,再根據(jù)頭文件生成頭文件保護宏就很容易了,這里不做說明了。
5.2.3按照要求生成拆分后的頭文件
為了保證我們的拆分生成的頭文件是自滿足的。通過上面分析,我們期望的每個結(jié)構(gòu)體的頭文件定義中所需要包含的頭文件如下:
- 包含基本類型定義的頭文件
- 包含結(jié)構(gòu)體中定義需要的宏定義
- 僅包含結(jié)構(gòu)體中嵌套的子結(jié)構(gòu)體的頭文件
由于已經(jīng)提前確定了基本類型的頭文件和宏定義的頭文件,在生成拆分結(jié)構(gòu)體的接口頭文件的時候只有需要在最開始的時候包含進來就可以了。在處理的過程中,為了達到一次遍歷遺留系統(tǒng)的巨型頭文件,在掃描的過程中新建文件@test_struct_include_path,然后把結(jié)構(gòu)體中包含的子結(jié)構(gòu)所需要的頭文件先臨時添加到這個文件中,新建臨時文件@test_struct_file_path,把結(jié)構(gòu)體的定義內(nèi)容拷貝到這個文件內(nèi),最后結(jié)束之后再把兩個臨時文件拼接到一起生成最終我們需要的頭文件,下面為核心代碼邏輯樣例。
def generate_for_struct()
lines = File.open(@spliter_header_path,"r").readlines
lines.each do |line|
if (line.include?"#")
next
end
if is_type_def_begin(line)
@write_file = File.new(@test_struct_file_path,"w")
@include_file = File.new(@test_struct_include_path, "w")
@is_write_able = 1
@is_struct_type = line.include?("struct")
if(@is_struct_type)
@include_file.puts "#include \"l0-infra/base/BaseTypes.h\""
@include_file.puts "#include \"ce_defs.h\""
end
end
if(@is_write_able == 1)
@write_file.puts line
end
if is_contain_struct(line)
include_struct_name = get_struct_name(line)
add_include_file_path(include_struct_name)
end
if is_type_def_end(line)
structname = get_struct_name(line)
do_generate_head_file(structname)
@is_write_able = 0
end
end
end
最后我們還需要處理一種特殊情況,就是對結(jié)構(gòu)體的名字進行重定義,代碼如下:
def do_generate_redefine(line)
struct = /[TE]_[\w]+/.match(line)
include_struct_name = struct[0]
add_include_file_path(include_struct_name)
temp = line.gsub(struct[0], " ")
struct = /[TE]_[\w]+/.match(temp)
struct_name = struct[0]
do_generate_head_file(struct_name)
end
5.2.4在原項目中的效果
1)拆分之后的頭文件首先按照原來的不同子系統(tǒng)之間進行目錄隔離,如下:
2)然后每個目錄里面包含了原來公共接口下所有拆分的結(jié)構(gòu)體頭文件,如下:
3)拆分之后每個頭文件的形式如下:
使用這套ruby的自動化腳本之后,只需要在命令行中敲入命令1秒之后,原來子系統(tǒng)之間所有的接口頭文件都已經(jīng)按照自己的要求拆分完成。當我開發(fā)完成這套腳本之后,我們團隊也在第一時間使用了,后續(xù)團隊在開發(fā)和版本同步升級過程中再也不需要手動拆頭文件,很大的節(jié)省團隊這上面的時間浪費。
結(jié)束語
在對遺留系統(tǒng)進行重構(gòu)的過程中,首先需要解決一些糟糕的物理設(shè)計問題,通過可以經(jīng)過一些初級的重構(gòu)從而改善遺留系統(tǒng)的物理設(shè)計問題,然后才可以基于此之上才開始構(gòu)建測試體系,然后再深度重構(gòu)。