有時候,程式設計者的工作不是設計應用系統,而是設計、開發API,供其他的程式設計者用以開發系統。而API本身,也和一般的應用系統一樣,會基於需求的變化,而有版本的演化。

在前一回中,我們提到了API的版本變化,可能發生了影響層面較小的實作內容改變,也可能發生影響層面較大的介面變動。

當新版本的API僅僅只是發生實作內容的改變,而在介面上沒有變動時,便可以維持向下相容的特性,也就是說,原先使用舊版API的客戶端程式碼,在升級到新版API之後,並不需要做任何的更動,就可以繼續正常的運作。

但是,倘若API介面有了變化,這將會使得原先相依於該介面的客戶端程式碼,必須進行相對應的修正,才能夠維持運作。此即發生了不能向下相容的情況。

發生不能向下相容的情況,對客戶端程式設計者來說,是一件相當麻煩的事情。因為在客戶端程式碼中,倚賴API所改變的介面處可能散佈在各處,必須一一找出來,並且進行相對應的修改。

而這修改程度,影響小的可能只是更改名稱,只需要更換程式碼中所使用的名稱即可。但大也可能關係到整個程式設計的模型或架構都完全改變,客戶端程式碼必須做大幅度的調整才能因應。不論如何,影響到客戶端的程式碼,都會造成程度不等的衝擊。

所以,除非可以帶來明顯又夠多的好處,否則API的設計通常會盡量維持向下相容性。

以擴展語法、語義的方式來修改,兼顧向下相容性

那麼,在設計上要如何保持API介面的向下相容性呢?在前一回中,我曾提到,介面的長相可以說是「語法(syntax)」,而其行為可以說是「語義(semantics)」。無論是語法或是語義的變動,都可以算是API介面的變動。

當我們必須引入新的語法或是新的語義時,都會造成API介面的改變。改變是勢在必行,但是改變不必然破壞向下相容性,只要這個改變不會影響到使用API舊語法,或舊語義的現有程式碼即可。要怎樣才能做到這件事呢?一個最簡單的方式,就是利用「擴展」語法或語義的方式。

舉例來說,就像微軟的Windows作業系統API時常使用的手法,某個原有的API函式名稱可能名叫LegacyAPI,但是,隨著時空環境的演變,設計者發現LegacyAPI的功能不足以滿足新的需求了,但是,已經有眾多既存的應用程式,都在程式碼中呼叫了LegacyAPI,一旦直接修改LegacyAPI的行為(語義),或是傳入的引數列表及回傳型別(語法),就會影響到這些既存的程式碼。

而他們常用的解決之道,就是透過增加名為LegacyAPIEx的API(其中 Ex 指的是Extend,即擴展之意),來提供新增的API語法或語義,如此一來,就不會影響到舊有的既存程式碼。

而要使用新語法或語義的新客戶端程式碼, 則直接使用Ex 的API版本。

這樣子來解決問題,當然是比較簡單的手法,但是當需要持續,也就是不只一次擴展API的語法或語義時,就必須不斷增加新的API函式,於是接下來會有 Ex2 、Ex3。

一旦增加的次數多了,就會失去API設計該有的簡潔特性,同時也容易造成客戶端程式設計者的混淆,因為很難從名稱上,去區分名稱類似的各種不同擴展版本間的差異。

這種情況,對於使用支援重載(overloading)特性語言的API來說,問題就會少了許多。

像C++或Java之類的物件導向語言,都支援函式的重載,這使得以C++或Java寫成的API,在擴展函式的語義或語法時,可以使用同樣的名稱,同時搭配不同的引數列表,藉以提供不同的語法及語義。這麼一來,雖然會有多個同名的函式,但是起碼它們的名稱相同,在簡潔特性上不致於大打折扣。

擴展API函式時,使函式因而變得更通用
很多時候,我們在「擴展」同名的API函式時,其實,往往是試著讓這個函式更通用化(generalized)。也就是說,因為我們察覺到原先的API過於局限,只能滿足特定的需求,而更廣泛的需求浮現了,所以我們需要一個更通用的版本──可以涵蓋原有需求,但又提供更多的可能性。

換言之,在實作時,儘管仍然維持舊有的API,但是因為已經引入了更通用的API,所以改版之後,舊有API大多都沒有真正的實作,而是直接運用新版、更通用的API來達成舊有API的作用。

從類別上擴展

有些時候,擴展的動作不只發生在函式名稱這種規模的層次之上,而是發生在類別層次上。同樣的,擴展是基於理解了更通用的需求,想提供更通用的功能,只是變化的範圍,不僅影響到個別的函式,而是影響到特定的類別。舉例來說,在JDK 1.1之前就有個java.uti.Vector的類別,它是直接繼承自java.lang.Object,並且提供我們所熟知的「Vector」的作用。

但是,到了JDK 1.2時,API的設計者認為整個Collection的API需要做大規模的重新設計,因而在繼承體系上做了大幅的調整。在JDK 1.2之後,java.util.Vector成了繼承java.util.AbstractList的類別,並且透過AbstractList得到了許多通用的功能。即使Vector也因為這樣的重新設計,經由繼承得到了一些新的函式,但是,舊有的函式也都獲得了保留,例如在JDK 1.0時代就存在的addElement()函式,即使在JDK 1.2之後,透過AbstractList得到了add()函式,但addElement()依舊存在。

事實上,「叫用通用版本」仍然是擴展之後最常看到的手法,所以介面上維持addElement()繼續存在,但它不過也只是叫用擴展後的add()罷了。

擴展可以發生在函式層級、發生在類別層級,當然也可以發生在一整個類別族系。基本上其精神就是維持舊有介面存在、行為不變,使得舊版本的語法及語義,得以繼續使用下去。但是,因為擴展後得到的是更通用的語法及語義,所以原有介面的實作,都可改用新版實作,而不需要有所重複。

透過擴展,一方面提供更通用的功能,一方面也能維持向下的相容性,但是,畢竟還是留下了額外的負擔,也就是舊有的介面必須存在。以上述所提到的Vector為例,同一個類別就必須同時擁有addElement(),以及add()這兩個作用其實一模一樣的函式。這說明了設計API時在介面的設計上需要更多的思量,實作的內容易於演化,但是介面一旦制定,要再加以更動就是大事了。

這同時也告訴我們,即使當前的需求十分局限,並不需要太通用的實作,但是,我們寧可在設計時提供更通用的介面,來包裝特定的實作,日後再來逐步擴充實作,也不要在一開始就設計出受限的介面,接著才在日後逐步的加以擴展。

一般來說,現在大多不鼓勵設計者「過度工程化(over-engineering)」,希望設計者盡量只先滿足當前的需求,不要對未來的需求有過度的想像,在設計當下便加以考慮,而在日後才逐步的演化。

但是對於API設計來說,由於更動介面所造成的影響甚大,要保有向下相容性,勢必也需要付出一定的代價,所以,在設計API時,就必須在過度工程化的可能性,以及保有適度的擴充性之間,取得一個好的平衡性了。

 

專欄作者

熱門新聞

Advertisement