如果系統希望和其他廠商程式庫搭配使用,若程式庫與客戶端只有少部份介面的不一致要銜接,那麼運用轉接器(Adapter)就足夠了;如果要全面銜接整個程式庫,或希望可以易於抽換程式庫提供者,則可試著識別出客戶端如何依賴程式庫,將依賴部份剝離出來規範為標準介面,形成橋接器(Bridge)架構,以避免客戶端與程式庫耦合問題,讓客戶端系統及程式庫都可以獨立地變化。

銜接介面差異的轉接器
物件間互動時介面的不匹配,時有所見,也許是在開發程式的過程,方法或程式片段中已運用某些介面進行操作,而現在打算引入了其他既有類別,或者是第三方程式庫,但發現到相關類別的介面,與客戶端並不相容。

舉個簡單的例子,JDK5開始提供了增強式for迴圈的新語法,可以搭配陣列或Collection物件使用,循序地取出容器中收納的元素。然而某些物件本身的資料結構,亦具有循序的特性,像是String就是由循序字元組成,若想讓既有的String實例,也可搭配增強式for語法,以逐一取得組成字元,該怎麼辦?

JDK5考慮到這個需求。實際上不必是Collection類型的物件,只要具有Iterable介面的物件,都可搭配增強式for語法,過去JDK版本就已存在的String,雖然不具備Iterable介面,但可定義IterableString實作Iterable,將想迭代的String加以包裹,就可使用增強式for語法,來逐一取得組成字元。

在《Design Patterns: Elements of Reusable Object-Oriented Software》書中,定義了轉接器模式:「將某類別的介面轉換為客戶端預期的介面。轉接器讓介面不相容的兩個類別可以彼此合作。」其中(以及本文)的介面,並非Java中的interface,而是指可操作的外觀或方法簽署(Method signature)。

從客戶端角度來看,轉接器隱藏了被轉接者(Adaptee)的介面,實際上,客戶端並不知道它對轉接器的操作,會委託給被轉接者。以上例來說,IterableString隱藏了String的介面,IterableString的角色是轉接器,而String就是被轉接者,然而作為客戶端角色的增強式for語法,只會知道操作的,是具有Iterable介面的物件。

轉接器模式是易於理解的模式,現實生活中最常見的比擬,就是插頭與插座的關係,牆面上若是兩孔插座,而筆電是三孔插頭,那麼可使用三孔轉兩插頭的轉接器,來解決插電的需求。

從轉接過程中識別出標準介面
轉接器主要是用來解決局部介面不一致的問題,如果用來全面銜接其他廠商的程式庫,或者更進一步,打算替換客戶端原先依賴的子系統,就會面臨為了配合子系統架構,而實作出的複雜轉接器架構;由於客戶端必須實作與維護轉接器,如果被銜接的程式庫介面有所變動,那麼原先針對子系統介面而撰寫的轉接器,也要逐一修改。

當面臨到系統與其他廠商的程式庫銜接問題日益複雜時,可識別出轉接過程中,到底是透過哪些客戶端預期的目標(Target)介面進行轉接,將該部份抽離出來進行標準規範,客戶端原先依賴的子系統也改為實作標準介面,而轉接器也針對標準介面進行實作。

換句話說,就是從原先「客戶端<-預期目標介面<-轉接器<-程式庫」的銜接方向,轉換為「客戶端<-標準介面<-轉接器<-程式庫」,由於客戶端原先依賴的子系統也針對標準介面實作,該部份會成為可替代的程式庫提供者之一,系統的其他部份可以繼續演化,這就具有橋接器模式的概念。

會採用轉接器來銜接程式庫,通常對於程式庫並沒有擁有權,而容易因為程式庫變動而受到牽連的原因,就在於此,程式庫一變動,轉接器就得跟著變動。由於程式庫提供者通常對程式庫具備擁有權,如果先前提到的標準介面,可由程式庫提供者實作轉接器來進行銜接,客戶端就不會因為程式庫介面或功能變動,而受到牽連。

橋接器令抽象與實作可獨立演化

如果有個應用程式正在開發,而系統採用某程式庫實作,隨著需求不斷增加,系統也定義出許多功能介面,對該程式庫的依賴也就日益加深,如果採用的程式庫有了功能變動,就得找出系統中相關部份逐一修正,如果今日想要更換為另一套程式庫,那麼修改系統中相關部份,就會是個重大工程。

先前從轉接過程中識別出標準介面的概念,可以拿來應用,識別出系統中實際上對程式庫依賴的目標介面為何,針對該部份進行標準規範,系統基於標準規範進行設計與演化,並要求程式庫提供者實作標準規範。

JDK中的具體實例之一就是JDBC,它定義了一組標準介面,系統可基於JDBC標準介面進行設計,資料庫廠商則針對標準介面進行連線程式庫實作,由於系統連線資料庫時不依賴特定的程式庫介面,因此可針對不同需求或不同資料庫,更換不同種類的程式庫提供者。

由於JDBC又考量工廠方法(Factory method)等設計,讓Driver實作物件作為單一入口,因此可透過組態設定Driver類別名稱,而更換整個連線程式庫。

在《Design Pattern》書中,定義了橋接器模式:「將抽象(Abstraction)與實作(Implementation)解耦合,令雙方可以獨立地演化。」實際上這邊所謂抽象,並不只是指靜態語言中用來強制規範介面的機制(像是Java中的abstract class或interface),而是指一個抽象系統,畢竟相對於現實世界,程式就是在一個抽象系統。

書中所謂的實作,則是指實現應用程式這個抽象系統時具體採用的程式庫或方案。實際上應用程式會演化,程式庫也會演化,兩者也都會有自身的抽象設計,橋接器的設計,目的在使得應用程式與程式庫在各自演化過程中,不致於彼此牽動。

舉例來說,應用程式有存取檔案系統的需求,而對於檔案系統可以有多元化選擇時,那麼,應用程式就不應直接使用存取特定檔案系統的程式庫,而應規範出存取檔案系統時的標準介面。

一般而言,應用程式基於標準介面進行檔案系統存取,使用者不會知道底層實際存取機制,因為這會由檔案系統提供者,根據標準介面進行實作。

像是Hadoop就具有抽象化的檔案系統,客戶端可透過Hadoop的FileSystem等API進行檔案存取,實際上底層實作可採用Local、HDFS、HFTP等針對本地、分散式或不同協定的檔案系統。實際上,JDK7中也對檔案系統的存取定義了標準,讓應用程式可基於標準JDK而採用不同的檔案系統提供者。

局部銜接還是整個取代
轉接器與橋接器經常被拿來比較,因為兩者都有隱藏程式庫提供者的作用。

先前看過,僅使用轉接器的銜接方式是「客戶端<預期目標介面<-轉接器<-程式庫」,而調整為橋接器後,則成為「客戶端<-標準介面<-轉接器<-程式庫」,可看出橋接器實際上還是基於轉接器,只是就客戶端而言,看到的是預期目標介面或標準介面。

如果新的應用程式正在開發當中,而使用的程式庫亦在持續演化,或者是考量到替換程式庫提供者的可能性,那麼很明顯地可直接採用橋接器,以讓兩者彼此獨立演化。如果既存的系統已使用某程式庫,現考慮採用其他廠商程式庫,那麼採用轉接器或橋接器的考量點在於,實作轉接器時使用的目標介面,是使用系統已存在的介面,所以既有系統不用什麼修改;然而為了避免產生複雜的轉接器架構,若非只是要局部銜接不一致的介面,而是希望可以替換整個程式庫提供者,那麼逐步將系統依賴的目標介面剝離,成為標準介面,運用此標準介面修改系統相關部份,從而實現橋接器模式,會是比較好的選擇。

作者簡介


Advertisement

更多 iThome相關內容