想要撰寫具可攜性的程式碼,應從設計上著手,透過介面一刀切開與平臺相依,以及與平臺無關的部分,這是個很重要的技巧。決定這個介面的內容,則是設計的重頭戲所在。

如果我們希望程式碼能多善用平臺特性,那麼就會限制住它可攜的範圍。例如對於跨平臺執行緒的控制,倘若你想在介面中加入設定、取得「執行緒優先序」的概念,那麼便會因為並不是每個平臺都支援執行緒優先序的設定及取得,使得這樣子的設計,可能不適用於各類型的平臺。

因此,程式人必須權衡目前及未來所設定的可攜範圍,依據究竟要支援那些平臺,來決定如何設計。很明顯的,越抽象的介面,通常能支援的範圍越廣泛,反之,則支援的範圍則越狹隘。

設定條件編譯定義,可能使程式變得一團混亂
我們可以將所設計的介面稱為具可攜性的介面,是因為基於這介面撰寫程式,便能與介面之下的平臺實作無關,因而獲得可攜性。

我分別以程序式語言(以C為例)及物件導向程式語言(以Java為例)介紹具體的實作方式。

倘若你使用的是像C這樣子的程式語言,想要撰寫可攜性程式碼,有一個最基本的技巧,便是利用條件編譯的指令。所以,程式碼可能是這樣寫的:

#ifdef _WIN32_
#include
#endif
#iddef _MACOSX_
#include
#endif

void functionDependsOnPlatform(void)
{
#ifdef _WIN32_
….
#endif
#iddef _MACOSX_
….
#endif
}

這種實作的技巧,是在每個與平臺特性相關的地方,不論是在函式中或甚至是含括標頭檔處,都利用像#if … #endif的條件編譯指令,來控制在不同的平臺上相對應的程式碼。

如果單純只倚靠這個方法,事實上並不需要在介面上多做設計,只需要利用條件編譯的指令,區分各個平臺上相異的程式碼。當想要可攜的平臺數少,而且平臺相異的部分不多時,使用這樣的方法是個不需要在設計上花費太多力氣的選擇。

但是,當你想要支援的平臺多,或者平臺相異的部分多時,整個程式就會變得一團混亂。

舉例來說,可能針對X動作,平臺A和B是一樣,但C不同;但針對Y動作,B和C相同,但和A則相異。對於讀者來說,只能透過條件編譯的指令,了解在X、Y動作上各個平臺的差異,卻缺少全貌性的認識,不容易系統性地獨立理解個別平臺的完整特性。

而當你要新增一個平臺的支援時,也必須找出這個平臺上,可能在那些動作上需要利用條件編譯,然後逐一地加上。
這種做法最大的缺陷在於程式碼的管理。

一來程式的架構會十分零亂,每個平臺的特性散落在程式碼四處。二來,要增加一個新平臺的支援時,沒有辦法系統性地直指因平臺而異的那些部分,只能憑記憶或者逐一檢視所有程式碼,以找出需要為該平臺撰寫專屬程式碼的地方。

定義一組函式作為隔離平臺差異的介面
僅利用條件編譯隔出各個平臺相異處的做法,之所以會造成程式碼的混亂,是因為這種做法,並沒有利用一道明確的介面,隔出與平臺無關,及與平臺相依的部分,因此所有的程式碼都混在一塊了。

對於C這類的程序語言,要建立一道介面,其實有一個簡單的方式,便是定義一組函式。你可以將各個平臺相異的部分,定義為一組操作(Operation),而每個操作,皆以一個函式表示。接著,針對每個平臺各自實作所有的函式。

舉例來說,Socket的API對許多我們常見的平臺,差異十分小。但是,在Windows上,和其他泛Unix平臺有幾個差異,例如:初始化Socket API(Windows上的Winsock API需要初始化程式庫,但泛Unix平臺則不用)、讀取Socket(Winsock用的是recv(),但泛Unix平臺則是read())、對Socket寫作(Winsock為send()而泛Unix平臺則為write()),及關閉Socket(Winsock為closesocket()而泛Unix平臺則為close())。

倘若我們希望程式碼在操作Socket時,能夠同時適用於Windows及其他泛Unix的平臺,那麼便可以建立一道介面,其中含有4個函式,分別是:

int initSocket(void)
int readSocket(int sd, void *buf, int nLen)
int writeSocket(int sd, const void *buf, int nLen)
int closeSocket(int sd)

那麼,再針對Windows及泛Unix的平臺,分別撰寫這4個函式的實作。你可以選擇將兩組函式的實作,分別置於不同的目錄下,在編譯時,依據所在的平臺,選擇相對應的實作編譯與連結。

這種做法的好處是,與平臺無關、以及相依的程式碼部分,都被一道介面(4個明確定義的函式)分開,架構十分清楚。透過這組介面,程式人可以明確地知道,哪些是與平臺相依的。

此外,要增加一個新的平臺支援時,除非這一組函式已不敷使用,否則只需要針對這一組函式分別實作即可。

當介面不足以表示平臺差異,即擴充函式
有時候,原先程式人所設計的介面,是針對已知要支援的平臺而設計。但是這個介面有可能因為欲支援的新平臺,和已支援的諸平臺間的差異,而無法滿足需求,導致必須擴充介面,才能夠維持可攜性。

舉例來說,在Mac OS X上,當用戶端和伺服器間Socket連線出錯誤時,會產生SIGPIPE這個信號,為了簡化處理,我們希望不要產生這個信號,因此,在建立Socket或是在接受用戶端連線之後,會利用setsocketopt()函式設定SO_NOSIGPIPE選項。而這麼一來,原有的介面便不足以表示各個平臺的差異。因此,程式人得擴充介面。

我們可以為這個介面加入兩個函式,分別是createSocket()以及acceptClientSocket(),代表建立Socket以及接受用戶端連線的函式。然後再為各平臺分別實作這兩個函式。針對Mac OS X的實作,在建立完Socket或是接受連線後,都額外呼叫setsocketopt()函式來設定SO_NOSIGPIPE選項。

擴充介面後,也要修改原有的程式碼。程式人必須把舊有程式碼中,替換掉與createSocket()以及acceptClientSocket()作用相同的程式碼。才能使這些程式碼成為具可攜性的介面。

為避免過度工程化,可邊做邊調整介面
持續地擴充介面是撰寫可攜性程式碼難以避免的情況。因為,通常在初期,只會決定支援特定個數的平臺,而在之後陸續增加支援。

有時候我們可以預見未來可能會支援的方向,預先在介面的設計上做好準備。但是這種預見,倘若想得太遠,又很容易成為過度工程化(Over-Engineering),因此,最常發生的情況,還是一邊增加新平臺支援,一邊調整原有的介面及程式碼。

我們在加入新平臺支援的同時,會對這個可攜性的介面進行一般化(Generalize),讓這個介面越來越抽象,也才能包容越來越多的平臺。

作者簡介

作者簡介─王建興

清華大學資訊工程系的博士研究生,研究興趣包括電腦網路、點對點網路、分散式網路管理、以及行動式代理人,專長則是Internet應用系統的開發。曾參與過的開發專案性質十分廣泛而且不同,從ERP、PC Game到P2P網路電話都在他的涉獵範圍之內。

熱門新聞

Advertisement