API本身的設計,若是遇到舊有功能無法滿足客戶端程式設計者的需求時,勢必進行改變。只不過,改變不意謂著必然傷害到API的向下相容性,因為倘若能在不動舊有API介面的基礎上去做擴充,那麼使用到舊有介面的客戶端程式碼,便不會被這改變所影響到。因為API介面只是做了擴充,擴充的部份不會波及到舊有的客戶端程式碼。

在前一回中,我們探討了一些在保留舊有API介面的前提下,如何擴充以便繼續維持向下相容性的方法。不過,除了擴充的手法之外,還有一些其他設計的技巧,有助於我們保持API的介面盡可能的不會需要做變動。一旦不需要變動,就不會衍生出向下相容性的問題。

從封裝的角度來看API的設計
其實設計的原則放諸四海皆準,不單是對普通的程式碼如此,對API設計亦然。例如我們時常強調「封裝」的概念,只是很簡單的觀念,卻十分有用。對API而言,其實可以區分成三個層次來討論。

第一個層次是介面,也就是我在前文中所提到的「語法」,它是客戶端用來操作API的表述方式。例如API中的函式名稱、引數列表中的型別、以及回傳型別、等等。

第二個層次則是API的行為,也就是在前文中所提到的「語義」,它定義了API介面下該有的行為。例如,呼叫了一個函式,應該得到什麼作用、回傳什麼結果。

而第三個層次就是API的實作細節,也就是具體實作API「語義」所定義該有的行為。只有「語法」及「語義」的更動,才有可能衍生出向下相容性的問題。理論上,實作細節的更動是不會破壞向下相容性。但,這是在客戶端程式碼完全沒有碰觸實作細節的情況,一旦客戶端程式碼和實作細節有所關聯,那麼,當新版的API在實作細節有所變化時,客戶端程式碼就會受到影響──而這原本是每個API設計者都假設不會造成向下相容性問題的情況。

封裝實作細節的重要性

實作細節的變化,是API之所以改版的常見原因。我們之所以需要有介面,便是希望透過抽象化的介面,來隔離客戶端程式碼對實作細節的倚賴,使得我們在改善API的實作時(像是修正錯誤或是提升效能),不致於影響到客戶端程式碼。但是,若是對實作細節的封裝做得不夠好,那麼就會暴露原先該對客戶端隱藏的實作細節,使得客戶端程式碼知悉實作細節,也就有可能進一步產生倚賴,那麼就會建立相依關係。這麼一來,API實作的變動,就會造成客戶端程式碼必須連帶變動。

對實作細節的封裝做得不夠好,會發生什麼情況?就會發生實作細節經由API的「語法」及「語義」洩露出去的現象。實作細節藉由API「語法」洩露出去,意即在API的「語法」上綁定了實作細節;而實作細節藉由API「語義」洩露出去,意即在API的「語義」上綁定了實作細節──也就是其行為和實作細節有關,是很常見到傷害到向下相容性的情況。

我們舉一個例子,假設你的API函式會需要在執行過程中進行排序,這意謂著回傳的結果會和排序的行為相關。

可是問題來了,排序其實分成兩類,一種是穩定排序法(stable sorting),而另一種則是不穩定排序法(unstable sorting)。了解排序演算法的人都知道,這兩類的排序演算法對同樣的輸入資料,可能會產生不同的排序結果。

倘若在API的語義上不規範究竟排序的行為為何,那麼,意指兩種類型的排序演算法都有可能成為API的實作方式。這麼一來,客戶程式碼有可能經由實際使用API的結果,了解到所使用API實作的行為究竟是採用穩定排序法,或是不穩定排序法。接著,發生了糟糕的事情,就是客戶端程式碼倚賴了這樣的行為。API本身的語義其實夠通用,因為它沒有規範所使用排序演算法的特性。

所以,當API的實作改版了,設計者也認為將實作從其中一類的排序演算法,更換為另一類時,並不會衍生向下相容性的問題。但是,因為客戶端程式碼已經倚賴了實作,所以,當API改版後,使用新版的客戶端程式碼就會被這個變動所擊中,而且無論是API設計者或客戶端程式設計者,都沒有意識到這次的改版會有向下相容性的問題,而這便構成了一個很大的威脅。

從這個例子中,當然也提醒了使用API的客戶端程式設計者,千萬不要倚賴API「語義」之外的「副作用(side effect)」。因為副作用不在「語義」的規範之內,是隨時可能跟著實作的改變而改變的。倚賴副作用,就如同在高空之中於鋼索上行走,是隨時都可能發生危險的。

所以,你接著一定能明白,身為API的設計者要盡可能的實現「資訊隱藏」的設計原則,絕對有助於減少向下相容性的問題。

就像物件導向設計中,希望程式設計者盡可能將所有類別中的資料欄位及函式的存取權限,設為私密(private),避免因為存取權限過於開放而造成類別的使用者知悉,進行產生相依關係。

每個資料欄位以及函式,都是實作的一部份。當使用類別的客戶端程式設計者有能力碰觸這些欄位及函式時,意謂著他們同時也相依於和這些欄位以及函式有關的實作。日後,這些實作必須有所變化時,這個變化就會透過相依關係影響到客戶端的程式碼。要注意介面設計的通用性

此外,介面設計的通用性對向下相容性,也影響深遠。因為,若是在一開始的版本裡介面設計的不夠通用,那麼當新的需求浮現時,便會察覺介面本身的局限,而造成了介面必須跟著改變。當然,我們也可以利用擴充的手法,在不影響舊有介面的情況下去增加新的介面,但這終究會為了向下相容性,而使得介面的長相不夠簡潔優雅。

例如,你可能設計一個用來排序的API,但最早的介面只能針對整數排序,所以,你會有個函式傳入的引數列表中,包括了一個整數陣列。可是,之後,你發現了浮點數的排序需求,所以又增加了一個可以針對浮點數陣列排序的版本。依此類推,直到你增加了一個可以處理任意型別陣列排序的版本為止。但這中間演變的各種版本,因為向下相容性的關係,都會一直存在。

當API不再能向下相容時
除了API的設計者可以為向下相容性盡力之外,API終究會有不向下相容的那一天。客戶端的程式碼其實也必須考慮到這一點,在自身的設計上考量到API不向下相容的風險。同樣的,可以透過抽象化的介面設計的技巧,來阻隔自身和API之間的變化。

就像Java的應用程式可以建立一個資料存取層來隔離JDBC,使得客戶端程式碼不直接倚賴JDBC API,而是面對這個資料存取層。那麼,即使JDBC API發生了不向下相容的情況,這個問題也會被該資料存取層所吸收、隔絕,而不會影響到真正的客戶端程式碼。

不論如何,永久維持向下相容性不必然是好的決定。必要時,還是必須選擇放棄一定程度的向下相容性,否則持續疊床架屋,並不是個好現象。不過,API向下相容性的失去可以提前預告,使得客戶端程式設計者有所準備。

現在常見的API,都會提前預告即將被捨棄不用的函式,建議客戶端程式設計者不要再繼續使用,同時也提供一個緩衝期,不在下一個版本就立即直接捨棄,而是等到之後的若干個版本後,再予以廢除。這麼一來,就可以提供足夠的時間,讓客戶端程式設計者評估改變的衝擊,以及進行必要的修改及測試。

 

專欄作者

熱門新聞

Advertisement