架構實現利器:反射

版權聲明:本文由韓偉原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/246java

來源:騰雲閣 https://www.qcloud.com/communitypython

 

做者介紹:韓偉,1999年大學實習期加入初創期的網易,成爲第30號員工,8年間從程序員開始,歷任項目經理、產品總監。2007年後創業4年,開發過視頻直播社區,及多款頁遊產品。2011年後就任於騰訊遊戲研發部公共技術中心架構規劃組,專一於通用遊戲技術底層的研發。程序員

通用型軟件框架的難題

假設咱們但願開發一套通用型的軟件框架,這個框架容許用戶自定義大量不一樣的狀況下的回調函數(方法),用來實現豐富多彩的業務邏輯功能,例如一個遊戲腳本引擎,那麼,其中一個實現方式,就是使用觀察者模式,以事件的方式來驅動整個框架。用戶經過定義各個事件的響應函數,來組織和實現業務邏輯。而框架也提供了自定義事件及其響應函數的入口。在一些實現代碼中,咱們可能會發現有大量的「註冊事件」的代碼,或者是使用一個巨大的switch…case…對事件函數進行分發調用。譬如咱們想作一個服務器端的基本進程框架,這個框架讓用戶只須要填寫一些回調函數,就能成爲一個穩定持續運行的後臺服務進程。其中一個部分,就是須要定義程序啓動事件,以便用戶自定義程序啓動要作的事情。那麼咱們能夠定義一個」Init」的字符串來表明這個事件,在一個事件響應函數的回調哈希表裏面,記錄上」Init」pfunInit()。又或者是用一個常量宏INIT=12來表示此事件,在程序的主循環處,利用switch…case…來檢查表明每一個事件的類型編碼,若是發現是和INIT宏相等的,就調用case INIT下面的代碼(每每是一個單獨的函數,如pfunINit())web

維護長長的「註冊事件」代碼和長長的switch…case…都同樣的讓人昏昏欲睡,同時容易讓人錯漏百出。這些代碼每每還帶有大量的「常量」,由於用來做爲回調函數的key的數據,每每都是一些自定義的常量。這些常量的同步維護,也每每讓人筋疲力盡。這些長長的代碼清單,常常還都須要由多個開發者一塊兒來使用,天然就很容易發生你錯改了個人,我覆蓋了你的這一類問題。這些問題很是的「低級」,可是要找起來卻一點都不容易。

[遊戲的按鍵控制代碼/JS]算法

難道咱們的框架代碼中,就必定會充斥着長長的字符串常量,或者整數常量嗎?答案是否認的,由於不少編程語言,都提供能反射的功能。在編譯型語言如C/C++裏面,也能夠利用代碼生成技術,模擬出相似反射的能力。數據庫

什麼是反射

要想知道什麼是反射,咱們能夠先來看一個觀察者模式的例子。假設咱們在編寫一個GUI的程序:在一個窗體上安放了一個按鈕,此按鈕的名字叫「ButtonA」,當這個按鈕按下的時候,咱們但願有一個咱們本身寫的函數被調用。根據觀察者模式的設計,這個按鈕被用戶按下後,程序底層應該能監測到這個事情,而後在進程內部產生一個「事件」,這個「事件」對象每每會帶有這個信息:被按下的按鈕名字。若是咱們用之前的註冊事件的方法來編碼,咱們必需要在按鈕被按下以前,好比程序初始化的時候,就向觀察者對象註冊這樣一個回調函數:RegisterEvent(「ButtonA」, ONCLICK, myOnClick) —— ButtonA被按下的事件—myOnClick()。這裏的函數myOnClick()就是咱們想處理ButtonA被按下的事件的響應函數。可是,咱們能夠用另一個更省事的方法來解決:咱們把myOnClick()函數的名字改爲ButtonA_OnClick(),而後觀察者在發生「ButtonA」被按下的事件後,自動去找有沒有叫「ButtonA_OnClick」這個名字的函數,若是找到的話,就調用這個函數。——顯然這種作法無需預先手工去註冊回調函數,而是僅僅根據函數名字的約定,簡單的來決定要調用什麼函數。通常來講,咱們認爲程序運行的過程當中,這些函數名字、類名字、屬性名字都不起什麼重要的做用,以致於咱們還會用一些「混淆器」軟件來處理源代碼,把這些自定義的名字都弄的亂七八糟,也不影響程序的運行。然而,若是咱們使用反射的技術,程序就能夠在運行時,實時的用一些常量,來檢索而且得到源代碼中,函數、類、屬性名字所對應的實體,而且還能調用這些東西。

[在Java裏經過字符串類名反射構建一個對象]編程

反射這種功能,在編譯型的C語言程序中,幾乎是不可以使用的,由於C語言源代碼中的名字「常量」,都被分離成「符號表」,而後在連接的過程當中從二進制可執行程序中去掉了。雖然動態連接庫會保留部分相似反射的能力,可是也僅僅限於動態連接庫的接口函數。在C++中,因爲編譯器支持RTTI(運行時類型檢測),咱們能夠經過typeof()操做符得到任何一個對象的類型信息,但咱們仍是不能實施用一個常量在運行時直接調用一個函數或對象的操做。不過,若是咱們使用IDL(接口定義語言)來用程序生成C++的源代碼,卻是能夠把對象構造器函數、成員函數等等的名字常量,做爲一個Map的key存放起來,對應把這些函數做爲value放入Map,這樣實現相似反射的功能(前提是要反射的對象都須要用IDL來描述,不然就要本身手工寫一堆註冊名字—函數的代碼)。設計模式

若是咱們使用基於虛擬機的語言,好比C#或者JAVA,又或者腳本語言,如python, Lua, JavaScript這些,都很是適合使用反射功能。因爲虛擬機在運行時是能徹底掌控全部代碼的「符號表」,因此使用語言系統提供的一些API,就能很方便的經過任何一個字符串常量,查找這個常量對應(在源代碼中)的類、方法、成員屬性等等。數組

反射的配置功能

在咱們懂得反射的用法後,咱們就能夠發現,源代碼再也不是「數據結構+算法」這麼簡單的東西。咱們能夠利用源代碼做爲數據自己的載體。一個最簡單的例子,就是XML的解析:咱們能夠定義一個和XML文件對應的類,這個類的成員屬性的名字,和須要解析的XML文件結構中的字段名一致。當咱們在解析對應的XML文檔的時候,就能夠經過XML內容中的字段名,找到對應類成員屬性對象,而後把XML字段值賦值進去。而這個過程當中,只要咱們按照XML文檔的結構來定義類,就能很方便的把XML文檔內的數據,賦值到一個類對象裏面,這對於編寫冗長的解析、賦值代碼來講,能介紹很多的代碼篇幅。這種作法也許不是很是高效,由於反射查找自己須要額外的CPU消耗,可是,若是解析XML這個步驟不是「關鍵路徑」,這點性能損失對比大段的相似代碼,仍是很值得的。服務器

反射用於配置的另一個功能,是把類名、方法名放在配置文件裏面,做爲程序功能的配置項。之前咱們若是想要利用配置文件,來定製一個程序的行爲,必需要在源代碼中編寫一段switch…case,來把行爲函數和配置文件中的配置值對應起來。這對於頻繁修改、增長這些可配置行爲的框架來講,是一個很是難以維護的工做。可是,若是咱們利用反射,就能夠直接在配置文件中寫入對應行爲的類名或方法名,這樣框架就能夠經過這些常量名字,在運行時找到進程空間中對應的類、對象、方法,從而直接調用他們以生效。這方面最多見的場景,有Tomcat這一類web容器,它們每每把一個個對應不一樣URL處理的servlet對象的類名,寫入到配置文件中。或者如Spring框架,把互相依賴的各個對象的類名,都用配置文件管理起來,在運行時根據這樣的配置文件,實時的反射出對應的類和對象,創建按配置要求的對象關係來。


[Spring經過XML來配置對象的關係]

從代碼維護的角度來看,類、成員、方法的名字,被程序之外的一些「配置文件」所管理和知道,是有必定風險的。由於咱們經常不把配置文件當作是源代碼那麼重要的東西,錯漏也沒有編譯器或者IDE協助,因此一些難以調試的BUG每每是從這些位置產生的。不過做爲一種大大節省框架代碼的技術,仍是受到普遍歡迎。而上文所說的問題,如今漸漸由另一種技術「元數據」(或者叫註解、特性),把配置文件和源代碼合併起來,這樣就能大大改善上述的問題。

反射的通訊功能

咱們在編寫通訊功能的程序時,傳統的思路是要定義協議,也就是定義協議頭部,協議包長度,協議包字段等等。在一個比較複雜的網絡服務程序中,這樣的協議很容易就有幾十上百個。維護代碼的程序員想要搞明白別人定義的如此衆多的協議,其實是不太容易的。咱們很容易想到,能不能使用對象模型來代替通訊協議的定義呢?答案是能夠的。可是,使用對象模型又有一個新的問題:對象是一個在運行時的內存結構,如何把對象中的數據,經過網絡接收和發送呢?最簡單的作法,就是使用memcpy(),Linux提供了這個功能強大的API,可讓任何內存中的數據變成一段字節數組,而後咱們就能直接經過網絡發送了。可是,若是咱們的對象不是一個簡單的結構體(事實上簡單的結構體也有問題),而是一個對象,這個對象裏面可能存在指針類型的成員,這樣的拷貝就不可能顧及到這些指針指向的數據了。並且,若是收發兩端的程序,並非同一種語言(操做系統、平臺),這樣的內存結構數據可能毫無心義,好比把一個C++的對象內存直接拷貝給JAVA程序,確定沒法直接使用。因此,咱們想要用對象結構來定義通訊協議,咱們須要一個把對象轉換成通用的字節數組的方法,這就是「序列化/反序列化」的能力。在這裏我不打算說太多關於序列化的內容,我只想說,當這些對象具有序列化能力後,就能成爲通訊數據的載體。問題是,若是咱們收到了一段對象序列化的數據,如何構建出對應數據的對象呢?答案就是使用反射,反射機能能從數據中得到對象類的名字,而後經過這個名字構造出對象來,而後從數據中繼續得到餘下成員的數據,一一複製到這個對象身上。由此看,只要咱們有反射功能,咱們可讓使用者,簡單的構造一個對象,而後整個把這個對象發送給網絡的另一端,對方也能直接收到一個對象,這樣在編寫通訊程序的時候,只要按照業務需求定義對象便可。對於閱讀代碼的程序員來講,不用在腦子裝一根叫「編碼、解碼」的弦,只要「無腦」的定義、處理對象便可。

在通訊程序中,有種叫命令模式的設計模式很是常見,它脫胎於傳統的基於命令字的網絡處理方式:解析出命令字經過switch…case調用對應的處理函數。命令模式下的通訊程序每每很簡單,就是定義一個類型,這個類型的成員屬性(通訊協議)是能夠隨便定義的,只要再定一個Process()方法便可——這個方法的內容,就是收到此類型對象,應該如何處理的容器。因爲咱們利用反射能夠在網絡另一段重建這個對象,因此咱們也能夠調用這個預約義的Process()方法,這個方法因爲和協議對象類定義在一塊兒,因此它是知道全部的成員定義的,這樣這個處理方法,就無需好像之前的程序那樣,費勁的經過強制類型轉換,來獲得具體的數據內容。在命令模式的通訊程序實現過程裏,反射是相當重要的一環,由於當咱們收到一個數據包時,必需要從數據包中獲得其對應的對象的類名,而後創建這個類所對應的對象。一旦這個對象創建後,咱們能夠調用其反序列化函數,讓對象的內容和數據包中一致,最後調用其Process()方法,就大功告成了。這種設計,能夠用不一樣的語言,定義同結構的類對象,用來在不一樣的語言平臺程序之間通信,而無需定義很複雜的協議定義規範。一些強大的對象數據工具,好比Google Protocol Buffer和Apache Thrift,直接能夠用一個通用的IDL語言,生成各類語言的類定義源代碼,就更方便了。

[Thrift、PB的自動序列化/反序列化的類型字段]

反射的編輯器功能

在我剛剛接觸Delphi這款IDE的時候,我驚歎於它那便利的功能:能夠對任何一個控件對象進行圖形化的編輯。雖然咱們能夠用初始化的代碼,來對任何一個對象進行修改,可是直接在IDE界面修改這些屬性,仍是很是方便的。甚至我會經過這些屬性界面,來猜想和學習一款控件的用法。像這類功能,每每背後就須要反射的力量(固然delphi可能不是使用反射,而是利用組件模版等技術實現)。當咱們本身開發一個這樣的程序,咱們必需要把一些對象、類的內部結構讀取出來,而後才能以另外的途徑展現出來。

[delphi上用界面設置ADO數據庫控件的屬性]

在JAVA中,JavaBean就是一個著名的利用反射來使用的「對象約定」:只要你編寫的JAVA類型,其成員是相似setXXX()或者getXXX()的,不少框架都會自動識別和處理這些成員函數,從而實現諸如自動更新成員數據,自動關聯界面內容等功能。另一個相似的例子是JMX,這個JAVA的通用監控標準接口,能夠把你定義的類對象解析出來,成員屬性的值能夠變成統計圖線、可修改的表格項,方法變成按鈕。在遊戲開發領域,反射還普遍的用於,把圖形美術資源和程序代碼結合的目的:好比Flash Builder就能夠經過反射,把一個Flash動畫對象,綁定到一個MovieClip類型上,從而得到一個既具有美術效果,又能讓用戶自定義行爲的對象。Unity3D在綁定了3D的遊戲對象和腳本組件後,對於腳本中的Start()/Update()函數調用,也是經過反射進行的,這樣開發者就沒必要要把腳本的類型,死死的和某個基類綁定到一塊,並且這些反射調用的函數,仍是能夠有不一樣的返回值(不一樣的函數原型),從而實現協程或者非協程的調用。


[在flash編輯器裏,對一個動畫指定關聯的自定義類]

反射因爲能夠把源代碼中的信息提取出來,和其餘的數據結合,讓源代碼的能力大大的提高,因此在開發工具方面,具備很是重要的地位。咱們再也不須要經過寫代碼,一遍遍的把源代碼的數據和外部結構作對接,而是簡單的開發一個反射能力框架,就能讓咱們實現某種源代碼的「約定」,從而實現各類豐富的快捷開發能力。

反射的最佳搭檔:元數據

在反射的使用過程當中,咱們每每會發現,源代碼直接做爲數據,仍是會有一些問題。譬如咱們的源代碼可能會根據一些非業務因數作修改,更名、改參數類型是在重構的時候很是常見的。因此咱們每每仍是離不開配置文件,把源代碼裏的名字寫到配置裏面,而後框架再根據配置來運行。一個比較典型的例子就是Hibernate,這一款著名的ORM框架,能讓你的源代碼類型和數據庫、表結構關聯起來。按理說利用反射,咱們能夠直接創建一些和數據庫表、字段名字同名的對象,就能直接關聯了,可是咱們的源代碼若是須要修改這些名字,再去改數據庫的內容,就顯得太麻煩了。因此咱們要編寫不少配置文件,來關聯什麼表對應什麼類,什麼字段對應哪一個屬性……這些配置文件每每和使用數據庫的表數量同樣多,任何的修改都還要記得對應這些配置的修改,咱們被迫同時維護:數據庫結構、配置文件、源代碼這三個東西。然而,若是咱們的平臺是支持「元數據」的話,問題就很好解決了。由於咱們能夠在源代碼裏面直接寫配置文件項目。咱們在源代碼的類名前面,用相似註釋的方式,標註這個類對應數據庫的哪一個表;在屬性名前面,用註釋標註對應的字段、默認值等等。這樣咱們只須要維護兩個東西:數據庫結構、源代碼。這大大的減輕的項目的複雜程度。

我接觸的最先最著名的元數據,是用來同步修改API文檔的JavaDoc技術,這個技術讓更新文檔再也不成爲一個苦力活。因爲能夠在源代碼的註釋裏面編寫文檔,因此在修改代碼的同時也能夠同時更新文檔。更重要的是,javadoc標記天然的把源代碼中的「名字表」和相關注釋自動對應起來了,要知道,這種對應若是人工來作,但是要費至關大的功夫。在javadoc的教育下,我對於java的註解、C#的attribute(特性)都以爲很是親切。之前那些須要登記大量類名、方法名的配置,通通均可以直接記錄在源代碼裏面了。而一些和美術資源關聯的客戶端代碼,也能夠經過源代碼的特殊標記,鏈接上正確的圖形資源。


能讓這些源代碼裏面的「元數據」生效的重要技術,其實就是反射。因爲咱們的元數據處理程序,通常都須要和源代碼裏面的類、方法名字對應起來,因此都要使用反射的方法。而這種反射,又爲咱們任意增長「元數據」提供了強大的機制。

反射給軟件開發帶來的改變

咱們曾經相信:數據結構+算法=程序。可是從今天的軟件產業來看,當然仍是有不少專事計算的軟件在被開發着,然而咱們接觸到更多的軟件,都是所謂「信息管理系統」類的軟件。這類軟件要處理的並不是是複雜的計算任務,而是對各類各樣現實世界中的信息,增刪查改是這些信息處理最通俗的描述。咱們在處理這些信息的時候,若是仍是把程序的載體源代碼,僅僅當作是編譯過程當中不可缺乏的一環而已,那麼咱們就必須額外處理大量的數據形式:數據庫、配置文件、IDE配置……然而,在面向對象的風潮之下,源代碼徹底能夠做爲一種「樹狀」的數據承載方式。面向對象定義的類、成員、方法,就是一個個現實世界中的實體映像,他們所包含的結構和常量,每每直接能夠成爲系統中的數據源頭。在MUD文字遊戲中,幾乎整個遊戲世界,都是以源代碼常量的形式編寫的,這不但沒有成爲維護的難題,反而讓真個遊戲的開發變得更輕鬆,由於程序員仍是最習慣於面對源代碼去工做。

反射這種特性,能把源代碼中的全部數據,包括「名字符號表」,都提供給開發者去使用,讓軟件開發過程,從單純的算法實現過程,變成一個綜合的信息管理的過程。這個作法看起來彷佛不夠專業,可是在編程已經不算「高科技」的年代,這種技術能幫助大量的開發者,以某種「約定」的方式去編寫源代碼,從而自動得到框架的強大支持。——製造這種容許「約定」方式運行源代碼的框架,正式新的框架應該擁有的特色,由於人類的創造時間,不該該被浪費在大量的重複而相似的工做之上啊!

相關文章
相關標籤/搜索