- Live軟件開發面面談
- 潘俊編著
- 4320字
- 2019-07-30 17:55:01
1.5 真正實現
要真正消除依賴,還需更進一步。調用方的代碼不能引用具體的編解碼器類型,不知道除接口之外這些類型的任何信息,但總歸要以某種形式知道這些類型。這看上去有些矛盾的任務如何完成呢?答案依然是通過第三方。調用方不能直接了解被調用方,但是某個第三方可以了解,調用方再去找第三方,而第三方本身是無關任何具體的被調用方的,是某種通用的習慣或標準的機制。最常用的第三方就是配置文件。
1.5.1 配置文件
假設在某個配置文件里記錄了每種媒體文件對應的編解碼器所在的程序集和類型名稱,播放器讀取該文件來創建所需的編解碼器對象。這個配置文件可以有一個專用的名稱,可以是某個播放器統一的配置文件的一部分,也可以每個編解碼器自帶一個名稱統一的配置文件。配置文件的格式有很多種選擇,INI、XML、YML……只要能滿足需求就行。下面就用服務定位器模式加配置文件來演示如何真正消除依賴。

這個類型用于解析和返回配置文件包含的信息。為了方便,這里提供一個專門的靜態方法返回所有的編解碼器信息,每條信息由媒體文件格式、對應的編解碼器類型名稱和所在的程序集名稱組成。

在播放器從服務定位器獲取編解碼器對象前,上述代碼先利用AppConfig讀取的配置文件信息為每一種媒體文件格式創建編解碼器實例。如果編解碼器較多,且創建成本高,也可以配合采用某種延遲創建(Lazy creation)機制等到播放器獲取編解碼器時才創建。
在現實世界中,利用配置文件來實現針對接口編程的例子也是很多的。Java的數據庫編程接口JDBC就是一個很好的范例。所有和數據庫的交互都是通過Connection、Statement、ResultSet之類的接口完成的,接口的具體實現則交給各個數據庫開發者提供的驅動器。這樣既使得數據庫使用者讀寫數據的代碼有通用性,又給了數據庫開發者最大的靈活性。所有讀寫數據庫的活動都是從Driver接口獲取一個Connection開始的,每個特定的數據庫驅動程序都要有一個類實現Driver接口。應用程序使用的具體數據庫的該驅動類的名稱就記錄在配置文件中,然后由DriverManager讀取并創建實例。過去配置文件是Java的系統屬性文件,后來可以是META-INF/services/java.sql.Driver,不過根本的機制都沒變。
1.5.2 配置代碼
比起使用配置文件,用代碼來提供同樣的信息簡單很多,這就是所謂的配置代碼。例如在AppConfig的GetCodecInfo方法里直接用硬編碼寫入編解碼器的信息。這種方式不是重蹈了1.4.2節中界定的覆轍嗎?確實如此,所以只有在一種特殊的情況下,這種方式才有正當性。
我們已經看出,包含被調用者信息的配置代碼,如果和調用者在一起,就仍然構成調用方的依賴。那么唯一可行的就是配置代碼既不屬于被調用方,也不屬于調用方。到目前為止,我們所處的開發環境都是調用者和被調用者可能由無關的兩方組織或個人完成,這也是需要消除兩者間依賴的現實原因。配置代碼不屬于任何一方,這就意味著又多出了一個新的開發方的場景。在此場景中,原有的調用者和被調用者代碼都作為可重復獨立使用的模塊公布,程序員利用這些模塊開發特定的程序。這些程序通常是非正式的、代碼較少的并且可隨需求和環境變動隨時方便地修改代碼的,它們對原有的調用者和被調用者模塊的依賴都無關緊要,配置代碼在這里就像方便的黏合劑一樣,免去更復雜和正式的配置文件。理論上對這些第三方程序,直接應用上文所述的三種模式也可以,使用配置代碼的好處,只是將配置信息和對象初始化等代碼分離開來,方便維護和修改。
下面用播放器例子來說明,這種情況就是播放器和編解碼器都是現成的組件,一個程序員利用它們開發一個能夠滿足業余愛好的個人播放器。
1.5.3 慣例先于配置
配置文件在整個軟件中發揮著很大作用。對用戶來說,它保存他們的個性化和偏好設置。對開發人員來說,它是用于存放程序運行所需各種信息的地方。這些信息既包括在程序開發時無從預知,只有在部署的環境才知道的;也包括那些通過編輯配置文件而無須修改代碼就能改變程序行為的。前者是不得不這么做,后者則是為了獲得靈活性的好處。兩種目的也不是涇渭分明,本節所分析的為了消除依賴而采用的配置文件就可以說兼而有之。
再好的東西太多也會成為麻煩。配置文件的方便使得有一段時期程序員大量依賴它,于是隨著組件、框架的增長,配置文件也爆炸式增長。配置文件大多采用XML格式,一個項目用到的類庫、框架越多,這些XML文件就越多。修改一個長長的、層次復雜的XML文件不是一件愜意的事,至少不像在IDE里編寫代碼那樣有那么多提示和錯誤檢查。修改配置文件,既需要專門的知識,又容易遺漏和出錯。為應對這種情況,有新的理念被提出。
一位餐館的熟客在點餐時可以說老樣子,而不用每次重復:一份回鍋肉,辣椒十成熟,肉八成熟;一碗西紅柿雞蛋湯,少放點西紅柿,少放點雞蛋,多放點水;一碗米飯,別加芝麻和香菜。在編寫圖形用戶界面時,控件的某項屬性如果和默認值一樣,就不用寫代碼設置。我們參加別人婚禮時,如果不是親朋好友的特殊關系,禮金就按慣例。
所有這些背后的理念都是相同的,那就是遵循某種慣例時,可以省去對該慣例包含的信息的描述,而活動參與各方仍然能夠順利溝通和合作。這個思想用到配置文件過多的問題上,就成了慣例先于配置(Convention over configuration)【注:這個原則的譯名有很多,約定優于配置、約定勝于配置、慣例優先等等,不一而足。然而都不夠準確。與約定相比,慣例更貼近Convention在這里的含義;Over表達的也不是優于勝于暗示的那種一方比一方品質更好、效果更佳,或者兩方發生沖突時慣例的效力更高(實際上正相反,當慣例不能滿足需求,必須使用配置時,配置的效力更高),而是作為手段的優先使用。慣例優先比較貼切,但又省略了配置,譯者可能也是考慮到慣例優先配置不符合中文的習慣??偠灾?,我以為慣例先于配置最符合原文的含義?!康拈_發范式。實際上,慣例在編程中早已大量存在和使用。每種語言的變量、函數命名規則,編碼時的格式規范,都是代碼的作者與讀者之間的慣例。但這些還只是為了人的方便,慣例的更大用途是讓程序的各方能相互溝通和合作,一個最不起眼的例子就是C和Java的本地運行程序都會有一個靜態的main函數作為啟動的入口,更復雜的例子包括Java文件所屬包和文件路徑的對應、Web項目內部的文件夾結構遵循一定的標準以方便開發時建構工具和運行時容器讀取所需的文件。這些隱藏的信息如果不是采用慣例的形式,就要引入配置文件,而程序要讀取這些配置文件,就需要它們的名稱和位置信息,這些信息不可能又保存在另一級配置文件里,所以歸根結底程序總是需要或多或少的慣例。
在消除依賴的上下文里,慣例發揮作用的形式很簡單。被調用者的具體類型的名稱只要遵循某種慣例,調用者就可以無須其他幫助便可找到它們。比如說每種媒體文件的編解碼器的類型名稱都遵循文件格式+Codec+版本信息的慣例,播放器就可以在某個第三方的編解碼器模塊里找到諸如MP4CodecV2的類型。
1.5.4 元數據
慣例的本質是一種合作各方知道的隱秘的知識。利用它可以節省明示的成本。不過慣例也有局限性。一是它的隱秘令外人不易了解,比起配置文件這樣的明示方式顯得不夠清楚。二是慣例的本質決定它只能適應單調的情況,無法滿足復雜和特殊的需求。例如在各種ORM(Object-Relational Mapping,對象關系映射)方案中,要建立對象屬性和關系型數據庫表字段之間的映射,我們很容易提出兩者之間名稱一致的慣例,但是因為種種原因,這個簡單的慣例不能滿足所有的場合的需要。遇到這些局限時,我們是不是只有采用慣例先于但不是取代的配置呢?Hibernate之類的ORM開始時就是這樣做的,長長的XML配置文件維護起來令人頭痛。幸好我們還有一件新武器——元數據。
顧名思義,元數據的意思就是關于其他數據的數據。比方說,一本書記錄了大量的信息(數據),那關于這本書的信息,諸如標題、作者、出版社,就是該書的元數據。代碼里的類、字段和方法等等同樣可以看作是數據,我們以某種形式來描述這些數據就是它們的元數據。最簡單的就是代碼的注釋。例如,我們都知道可以用某種約定格式的注釋記錄一個方法的用途、參數和返回值等信息,這些元數據既可以被IDE提取作為參考,也可以用專門的工具抽取出來制成完整的文檔(JavaDoc就是著名的樣例)。
元數據有時可以代替慣例給我們一種更清晰地描述信息的途徑。譬如單元測試的類型和方法名稱過去通常約定綴以Test,以區別于普通對象,并便于測試工具識別和運行。有了元數據,就可以給這些方法加上特殊的標記(如C#的Metadata元數據和Java的Annotation標注)。如下面這個采用JUnit標注的測試對象。

另一方面,元數據和代碼在一起,相較于獨立的配置文件,更簡潔直觀和易于維護。所以在Java中有了Annotation之后,Hibernate的對象關系映射就換成了這種方式。下面(來自Hibernate官方網站教程)分別采用XML配置文件和標注來建立映射的樣例就清晰地體現了兩者的差別。




針對消除依賴的主題,應用元數據的方式也很簡單。上一節末尾提到采用慣例時,媒體文件的編解碼器類型的名稱遵循特定的格式。如果采用元數據,就可以為編解碼器接口定義一個帶參數的標記,參數用于設定編解碼器所針對的媒體格式和版本號。每個編解碼器的開發者只要給其具體編解碼器類型加上該標記,播放器在加載包含這些編解碼器的類庫時,就可以利用標記找到所需的編解碼器。

在3.5節,還會給出用元數據消除依賴在現實世界中的應用。
1.5.5 實現消除依賴的方法的本質
在列舉了真正實現消除依賴的各種途徑之后,再來看看它們的共同點和本質。消除依賴要求調用者和被調用者僅通過接口溝通,而接口是不包含實現代碼,調用者無法創建實例的,所以調用者還是要在某個入口處創建一個具體實現接口的被調用者實例。創建實例時不能在代碼中用到該實例的具體類型(否則就產生了對它的依賴),也不能將這種方式的創建委托給其他對象(依賴有傳遞性),所以唯一可行的創建實例的方式是反射。反射時不能直接在代碼里寫明實例類型的名稱(否則就僅僅是另一種形式的依賴),必須通過某種約定的途徑獲得被調用者類型的信息,這些途徑主要包括配置文件、慣例和元數據。除了配置文件是顯式地說明被調用者的信息,采用后兩種途徑時,調用者依然要借助反射。
調用者利用反射來創建被調用者的實例。調用者通過配置文件、慣例和元數據來獲取被調用者類型的信息。這兩點便是實現消除依賴的諸方法的本質。
那平常被宣傳和介紹的工廠模式、服務器定位模式和依賴注入的價值何在呢?答案很簡單。它們的價值就是它們本身實現的功能。工廠模式能將某一系列的對象創建集中于一處,服務定位器模式方便調用者從單個地方獲取所需服務,依賴注入使調用者通過方法參數被動地獲得被調用者。總之,作為有普適性的設計模式,它們可以用在除消除依賴之外的各種場合,所以單純應用它們也就不能保證消除依賴。直接在調用者的代碼里運用上面所說的消除依賴方法的兩條原則,就能夠實現針對接口編程。不過為了使代碼功能清晰,通常我們會采用某種設計模式,將獲取被調用者實例的邏輯封裝在單獨的對象中。也就是說,工廠模式、服務器定位模式和依賴注入是實現消除依賴時兩條原則的封裝方式。