模組化與介面設計
在軟體變得越來越龐大的時候,人們開始探索模組化編程,允許軟體的某一部分功能封裝成一個模組,在軟體運行時由作業系統在磁碟上找尋對應的模組並載入使用,從此軟體可以劃分為相互獨立的模組,允許不修改軟體整體結構,僅單獨更新某個模組而增強某個功能。
動態連結程式庫
(DLL)
是一種很好的模組化技術,在軟體運行時,動態連結程式庫會被作業系統裝載,並與主體程式連結,主體程式通過調用動態連結程式庫中的函數實現某一個功能。在升級模組時主體程式不更新,因此動態連結程式庫中的函數在更新時必須保持與主體程式的介面不變,否則主體程式調用動態連結程式庫時將因為介面不匹配而出錯。
教科書和開發經驗都告訴我們,介面的設計要保持一定的穩定性,盡量減少介面變化帶來的維護工作量。
C語言的做法
Windows
系統
API
顯然有很強烈的介面不變性需求,因為
MS
也希望在他的作業系統升級後,舊版系統下的軟體無須太大的改動就可以應用在新作業系統中。
WINDOWS
API
是
C
語言介面,
C
語言的一種常見做法是對參數結構體定義一個
length
欄位,程式中通過判斷
length
欄位來區分不同版本的資料結構,這樣就可以做到高版本的介面相容低版本的介面
以
WINDOWS
API
中啟動進程的函數為例
BOOL WINAPI CreateProcess(
__in_opt LPCTSTR lpApplicationName,
__inout_opt LPTSTR lpCommandLine,
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in BOOL bInheritHandles,
__in DWORD dwCreationFlags,
__in_opt LPVOID lpEnvironment,
__in_opt LPCTSTR lpCurrentDirectory,
__in LPSTARTUPINFO lpStartupInfo,
__out LPPROCESS_INFORMATION lpProcessInformation
);
其中的參數
lpProcessAttributes
是
SECURITY_ATTRIBUTES
介面,定義如下
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
可以看到
SECURITY_ATTRIBUTES
中的成員
nLength
,他描述的是
SECURITY_ATTRIBUTES
結構的大小,文檔要求調用者指定傳入的
SECURITY_ATTRIBUTES
參數的大小,一般的做法是取
sizeof(SECURITY_ATTRIBUTES)
,可以推斷
API
的實現中將這樣區分
lpSecurityDescriptor
參數的版本
if(sizeof(*lpSecurityDescriptor)
== sizeof(SECURITY_ATTRIBUTES_NEWVERSION)){
//new
implementation of this API...
}else
if(sizeof(*lpSecurityDescriptor) ==
sizeof(SECURITY_ATTRIBUTES_OLDVERSION)){
//support
old version of this API...
}
這樣升級
API
時,介面定義不需要改動,當舊軟體放到新版本的作業系統中運行時,新版本的
API
可以繼續支援舊版本的調用程式,而在新作業系統下開發的新軟體又可以使用新版本API
的特性,實現了介面不變的功能增強。
只是有一個小負擔,就是多出了一個無業務意義,僅僅用來保持介面不變而設定的
nLength
成員,而且要求調用者編程時必須傳入
sizeof(SECURITY_ATTRIBUTES)
的值,但它確實達到了目的。
這種介面在
WINDOWS
API
中不為少數,在
UNIX
的庫中似乎相對少一點,但也存在,比如
socket
API
COM的做法
在
C++
世界,
COM
選擇了一種更為花哨的做法,通過介面包容來實現
定義一個介面
virtual
class IMyInterface1{
virtual
void doSomething() = 0;
};
編寫對象實現這個介面
class
MyObject1 : public IMyInterface1{
virtual
void doSomething();
};
通過
createInstance
建立
MyObject1
的對象並返回
IMyInterface1
,調用端通過
IMyInterface1
來使用功能。當介面升級時,定義一個擴充介面
virtual
class IMyInterface2 : public IMyInterface1{
virtual
void doOtherthing() = 0;
};
新介面支援
2
個功能
doSomething
和
doOtherthing
,所以必須編寫一個新類去實現新功能
doOtherthing
,並且希望舊功能
doSomething
的代碼可以繼續重用,新對象可以這麼寫
class
MyObject2 : public ImyInterface2{
virtual
void doSomething()
{
obj1.doSomething();}
virtual
void doOtherthing();
MyObject1
m_obj1;
};
當請求新對象的舊介面時轉到舊對象中去實現,當請求新對象的新介面時使用新實現,新版本的
createInstance
根據新舊對象的
GUID
建立新對象或者舊對象,返回新介面指標或者舊介面指標,舊的調用者同樣不需要作任何改變即可使用新的組件,而在新組件環境中編寫的新調用端又可以使用新介面,它用樣達到了介面不變的目的。
相比
C
語言的做法,少了用於支援介面設計的冗餘欄位
nLength
,但程式介面比
C
語言複雜了不少。
實際上
COM
的包容方案比上述稍微複雜一點點。另外
COM
還有另一個更複雜的方案-彙總,但彙總要求被彙總對象有附加的特殊設計,基本上被限定在
COM
的架構內,無法在一般
C++
程式設計中通用,這裡不討論。
SOA/SCA的做法
SOA
是近年提出的概念,其中
SCA
是
SOA
概念中的面向組件編程,既然是組件,介面不變性同樣很重要,
SCA
選擇了更強硬的做法-
SDO
SDO
是
Service
Data Object
的縮寫,說到底就是一個
XML
,利用了
XML
的高擴充性,
SDO
只是一個對
XML
解釋的封裝類而已,所有介面通過形如
class
MyObject1{
void
MyService(SDO myParam);
};
的匯出類來實現(匯出類還是
createInstance
不是關鍵,
SDO
類型的參數才是關鍵),
SDO
說到底是一個
XML
操作類,他與一個
XML
字串對應,與形如
class
MyObject1{
void
MyService(char *myParamXml);
};
是一個道理,說的更徹底一點這個介面就是一個啞介面,介面的意義完全由模組內部運行時解釋,業務強相關,與形如
class
MyObject1{
virtual
void MyService(void *myParam);
};
的介面的道理是一樣的,這種介面一勞永逸,無論模組怎麼升級、內部實現怎麼改,只要
myParam
的設計適當,
MyService
介面的定義是永遠不需要變動的,他永遠是
virtual
void MyService(void *myParam);
但無論是調用者還是被調用者,實現起來都比前面討論的方案更複雜、更多約定,也更容易出錯,並且所有錯誤都需要在運行時才能發現,沒法做編譯時間檢查。我的同時很喜歡
SDO
這種做法。
順便說一下
Tuscany
項目是
apache
組織的一個
SCA
參考實現,實現了
SDO
,
C++
版本目前還在
Incubator
狀態,一年多最近沒更新了,貌似此路不通。。。
一種字典的方案
工作過程中發現有另一種民間方案,有點土,但也有點實用,就是使用類似字典的表格來傳遞參數,只要字典類結構不變,介面也不需要改變,他形如
void
MyFunc1(Map param);
參數
Map
類是一個穩定的結構,所有參數放到
Map
裡面去傳遞,使用時
Map
param;
param[“name”]=”lingch”
param[“gender”]=”male”
MyFunc1(param);
當模組升級時,可以通過支援更多的
KEY
來增強功能,如
param[“name”]=”lingch”
param[“gender”]=”male”
param[“extended1”]=”handsome”;
MyFunc1(param);
新模組可以支援
extended
參數提供更豐富的功能,舊模組無法之此後
extended
參數可以直接忽略。他也得到了介面穩定的目的。
這種做法可以在
C++
中使用,
Map
類型要足夠穩定,可以使用
STL
中的
std::map
,還好
std::map
還是足夠穩定的沒怎麼變過,如果考慮
STL
有多個供應商則你可能要自己寫一個類
我發現
JAVA
一類的上層程式員更喜歡這種做法,他們用
Properties
來傳遞字典
另一種假想的方案
在思考介面設計的問題時,我曾經考慮過這麼一種方案,通過
C++
的
RTTI
機制來實現
(
這種方案沒有驗證過
)
,介面
API
設計成
void
MyAPI(MyClass *param);
在實現
MyAPI
時判斷參數類型
if(typeid(b_object).name()
== “xxx”){
//new
implementation of this API...
}else
if(typeid(b_object).name() == “yyy”){
//support
old version of this API...
}
這種方式與
C
語言的做法一樣簡單,並且參數結構裡少了
nLength
的冗餘欄位,但需要
C++
RTTI
特性的支援,而
RTTI
似乎不是所有
C++
編譯器都支援的,即使在
VC
裡面
RTTI
預設也是關閉的。
總結
以上討論了各種介面設計,我不打算評價各種介面的優劣,他們各有各的適用情境。介面設計是一個矛盾的問題,一方面我們希望介面穩定不變,以降低介面變動帶來的維護成本,另一方面,設計靈活的不易變的介面本身就給介面的實現和使用都帶來成本。對於這種矛盾的問題常常又變成一個‘度’的問題,要權衡。
可以從以下幾點來考慮模組之間使用哪種介面
模組的使用範圍:小範圍使用的模組變動成本低,無須靈活設計
模組的生命週期:沒有長遠發展計劃的模組無須投入精力設計靈活介面
程式員的技能水平:考慮程式員的水平是否足夠維護或使用該介面
學習成本:這套介面是否會引入大量文法知識使得學習成本增加
技術的選擇還是要以實際收益為依據