有時候,程式設計者的工作不是設計應用系統,而是設計、開發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時,就必須在過度工程化的可能性,以及保有適度
的擴充性之間,取得一個好的平衡性了。
--
plurk
http://www.plurk.com/dasea2017
face book
0911457604,0934169099
google+
https://plus.google.com/u/0/
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 61.58.22.74
※ 編輯: dasea2008 (210.66.169.48 臺灣), 04/26/2021 05:29:31
※ 編輯: dasea2008 (101.0.228.86 臺灣), 04/11/2022 23:33:44