透過Reflection,我們所寫下的程式碼,可以在執行時期才決定要產生什麼類別的物件、要呼叫物件的那一個函式,及存取那一個資料欄位。也就是說,程式人所寫下的程式中,可以完全不含有這些資訊,因而也就不受限於這些資訊,能夠動態容納的範圍也更大了。

Reflection最早的用意,是提供IDE視覺化的開發功能
總的來說,Reflection帶來一些優點,包括(1)協助程式人撰寫更具彈性、更易擴充的程式碼,(2)允許程式人檢視類別內部實作,(3)提供基於外貌式的多型(Signature-Based Polymorphism)。

Reflection能如何協助程式人撰寫出更具彈性、更易擴充的程式碼,在前幾回中已經詳述不少。而在檢視類別實作方面,透過Reflection,程式人夠得知任一個類別所擁有的資料欄位及函式,這在一些用途可以用得上,例如程式開發工具裡的自動補齊(Auto-Completion),便可以自動判斷程式人正在處理的類別,並自動地找出資料欄位及函式的名稱,協助程式人補上名稱。

像一些所見即所得的整合開發工具(IDE),當程式人從工具箱(Toolbox)拖拉了某個元件出來設計時,IDE便能夠允許你檢視、設定該元件的屬性值及事件處理常式(Event Handler)。

為什麼IDE能夠知道所有元件的屬性值以及有那些事件呢?IDE的設計者本身一定不可能知道、也不可能在設計IDE時,便在程式中寫死要支援的元件。而這樣的需求,剛好利用Reflection的機制便可以滿足。

Reflection這項技術最早被發展出來的用意之一,便是為了要提供IDE的視覺化開發功能。除此之外,Reflection所提供檢視類別內部實作的功能,也允許我們進行初步的反向工程(Reverse Engineering),因為透過Reflection,程式人可以得知類別內部有那些資料成員和函式,而這些都會曝露出它們的作用,使得程式人得以略為窺探類別的內部實作,並猜測運作方式。

有些時候,這樣的資訊便足以讓我們了解我們想要掌握的東西。

「外貌式的多型」──只要求多型的類別間,擁有同樣外貌式的函式而所謂基於外貌式的多型,和一般所認知的物件導向中的多型有什麼不同呢?

所謂的「多型」是不同的類別具有相同的外表、卻有不同的行為展現。在一般我們所認知的多型下,「相同的外表」是以共通的介面來約束。所以,當我們要利用多型的機制時,我們會進行向上轉型(Upcast),以便轉型至共通的介面(或是基礎類別),接著呼叫轉型後所得介面中的函式。

而基於外貌式的多型,卻不倚賴多型的類別間必須具有共通的介面,它只要求多型的類別之間,擁有同樣外貌式的函式即可。

有一個最簡單的外貌式多型的例子,便是Java應用程式啟動的方式(C#應用程式啟動的方式亦然)。所有Java應用程式的起點,都是某個類別的main()函式,而這個main函式的外貌式,必須是public static void main(String argv[])。當我們打算透過Java指令啟動某個類別時,可能會這麼輸入:

java foo.bar.MainClass

此時,Java的執行環境便會載入foo.bar.MainClass這個類別,接著檢查它是否具有上述外貌式的main()函式,倘若有,便利用Reflection的機制執行它,反之,便會輸出錯誤訊息,告訴你所指定執行的類別,找不到符合該外貌式的函式,因此無法執行。

Java執行環境的啟動程式,必須要能夠啟動所有可以被當做應用程式入口點的Java類別。這些類別我們可以說它們是具有多型行為的。但Java並沒有強迫所有可以被當做應用程式入口點的類別,都去實作相同的介面,以便啟動程式以多型的方式啟動它們(另一方面,多型也不適用於靜態函式)。

換句話說,Java的設計,選擇的便是以外貌式多型的方式,讓所有可以被當做應用程式入口點的類別,都提供一個具有相同外貌式的main()函式,來供啟動程式呼叫。

外貌式的多型可以讓多型更有彈性,因為多型變成以函式為粒度單位,而不是以介面(或基礎類別)為單位。而程式人所要操作的多個類別之間,即使沒有共通的介面或基礎類別,仍然可以寫下多型的程式碼。

Reflection的缺點:效能降低、複雜度提高
以上便是人們時常會提到的Reflection優點,但好用的工具總有兩面刃,即使Reflection具有這些優點,也不代表運用它時,不會帶來任何負面的效應。Reflection在運用時可能會帶以下這些缺點:(1)效能較低(2)程式複雜度升高(3)權限控制失效(4)可能涉及類別內部實作因而提高程式碼的相依性。

Reflection的執行效能較差是可以想見的,因為它改用更為動態的方式呼叫函式,或者存取資料欄位。效能和彈性總是站在對抗的兩邊,我們在從事設計時,時常都必須在兩者之間取捨。

身為一個程式人,可能既想要獲得Reflection帶來的彈性,又必須考慮它付出的效能代價。不過,一般來說,除非程式碼是極需效能,否則Reflection付出的效能代價,不會產生過於明顯的感受。

運用Reflection也要注意一件事,程式碼的複雜度會較一般程式碼來得高。一般程式碼只需要利用new之類的關鍵字便能產生類別物件,只需要利用像「.」這樣子的符號就能存取資料欄位或是呼叫函式。但是,運用Reflection的程式碼,卻需要透過多道手續才能完成同樣的工作。

就程式的可讀性來說,往往會低了許多。另外,運用動態的Reflection機制之後,便會喪失語言本身為程式人提供的靜態檢查機制。例如呼叫某個函式時,語言的靜態檢查機制,會幫助程式人檢查引數的個數、型別是否正確,但是使用Reflection機制來呼叫時,必須自行處理可能會發生的錯誤。這會讓程式碼必須額外增加許多異常的處理程式碼,無疑的,也會增加程式碼本身的複雜度及閱讀難度。

另外,Reflection也為存取資料欄位、呼叫函式開啟了一道後門。例如在Java程式語言裡,利用private、public等關鍵字來設定存取的權限。例如宣告為private的類別成員,便無法為外界所取用。但透過Reflection,便可以逾越這道界限,輕鬆地加以呼叫,而這可能會違反原類別設計者的初衷。

此外,一旦穿越存取權限這道高牆之後,程式人便得以任意運用Reflection,取用其他類別內部資訊的類別,而這便會違反了封裝以及資訊隱藏的原則,提高了它自身與被取用類別間的耦合程度。而這往往是我們所不樂於見到的。

正確使用Reflection,程式將更能因應變化
當程式人知道Reflection運用的優點及缺點後,會更明白應當在什麼樣的應用情境下使用Reflection。

正確地使用reflection,可以帶來彈性、擴充性,同時讓程式更容易因應需求的變化。

如果你想設計那種只憑藉著一顆引擎,便能夠驅動所有無限可能變化的程式碼,懂得運用Reflection肯定會帶來許多幫助。

專欄作者

熱門新聞

Advertisement