[Software Architecture]透過Reflection提高客製化彈性

[Software Architecture]透過Reflection提高客製化彈性

這是個有趣的議題,開發Package系統,絕對不能忽略客製化,一體適用的產品在市場上並不存在,如果不能提供客製化的功能,很多客戶是不買單的,而提供客製化的方法很多;

  • 繼承:透過提供virtual的function給子類別override來達成
  • 高度模組化:將功能分成很多的component,提高客製能力(或發展成SOA-like架構)
  • Code Generator:提供程式產生器,直接依客戶定義產出新程式
  • 其他:......

以上幾項單獨使用都會有一些不太方便的地方,因此最好的方式還是將以上幾項做些融合,客製的彈性、維護性、可版更性才會更好,這陣子討論的很熱烈的一個議題就是:能否不開Visual Studio就能完成程式客製化呢?

這個問題很好阿,如果可以讓客戶在家自己調整,不用學怎麼寫程式,只要知道工具怎麼用就好,乍看之下好像蠻好的,但如何做呢?如果是UI的客製,我們可能可以透過XML來定義UI結構,給客戶一份XML的說明文件(或者提供XML對應功能的工具),讓客戶自行依喜好設定。

那如果是邏輯呢?如何讓客戶自訂執行程序?我們看一下下面這個case。這是一個關於提款機提款功能的客製化,狀況應該蠻常見的,左邊是一般的提款程序,假設今天我希望在整個程序中加入兩個子程序,怎麼做?最簡單的改法自然是在程式中加入『調閱信用狀況』與『參加抽獎活動』兩段程式,聰明一點的可能順便加上參數化設定,讓活動結束後可以關閉此功能。

 

image

這樣的客製化問題,每天在資訊產業中上演,只要您有開發過系統,相信都有遇過這樣的問題,您是怎麼做?我相信絕大部分的人應該都是透過提高架構模組化,讓系統中的component可以被高度的reuse,只要能被reuse,就有機會被組合出新的功能了,而這種概念也是SOA架構一個很重要的精神,只要我們能將component(或稱服務)掛載於ESB上,系統有需要使用到時就到ESB上取得該服務,足夠模組化的系統,要做到相似的功能絕對是有機會的,只要程式的介面訂的夠靈活,要動態抽換功能或者增添功能,絕對都是有機會的,架構有點類似下圖(請先忽略合理性):

 

image

差點離題,今天我不是要討論ESB或者SOA,而是想談.net自己本身的另一個機制:Reflection(反射),Reflection會被提出來的原因是因為它是屬於晚期繫結(late binding),與一般的前期繫結(early binding)最大的不同在於他不需要在專案中加入Assembly參考,而是在程式執行過程中,動態被加載進處理緒中(少數案例在Assembly已經被參考後還用Reflection),而使用Reflection的寫法蠻單純的,如下:


            object tResult=null;
            Assembly tAss = Assembly.LoadFile(@"C:\ClassLibrary1.dll");
            for (int i = 0; i < 100000; i++)
            {
                Type tTypeOfClass = tAss.GetType("ClassLibrary1.MyClass");
                MethodInfo tRunMethod = tTypeOfClass.GetMethod("ReflectionTest");

                object tRunInstance = Activator.CreateInstance(tTypeOfClass);
                tResult = tRunMethod.Invoke(tRunInstance, null);
            }

上面這個範例我透過Assembly.LoadFile的方式載入一個位於C:\下的ClassLibrary1.dll Assembly,並呼叫裡頭的ReflectionTest function,過程執行100000次,而這個ClassLibrary1.dll在我設計時期,我並沒有將這個Assembly參考進來,我能呼叫它完全透過以下幾個點:

  • 路徑(C:\ClassLibrary1.dll)
  • namespace(ClassLibrary1)
  • class name(MyClass)
  • function name(ReflectionTest)
  • function parameter(null,沒有參數)

 

 

從Reflection的概念來看,我們不需要建立ESB,我們只要知道Assembly的資訊,並由設定中得知我們要組合哪些Assembly中的哪些功能,那應該就能做到不改Code而達到客製的功能了吧?原則上是這個樣子沒錯的,我只要將我要呼叫的功能,一個一個透過Reflection呼叫起來,我完全不用在一開始就將Assembly參考進來,非常好啊,不過我們不能忽略Reflection最被人所詬病的點:執行效能,由於Reflection是在Runtime決定型別,在整體的執行效率上確實不如前期繫結來的快速,以下針對這部分做了一些測試,這個測試分成四個部份:

  • 直接呼叫同一個Assembly中的function
  • 直接呼叫不同Assembly中的function(先參考進來才呼叫)
  • 透過Reflection呼叫同一個Assembly中的function
  • 透過Reflection呼叫不同Assembly中的function

直接呼叫同一個Assembly中的function,執行10萬次所需時間大約在0.35-0.45秒之間:


            //直接呼叫本Class中的ReflectionTest
            DateTime tStart = DateTime.Now;
            for (int i = 0; i < 100000; i++)
            {
                ReflectionTest();
            }
            DateTime tEnd = DateTime.Now;

            textBox1.Text = tEnd.Subtract(tStart).ToString();

直接呼叫不同Assembly中的function(先參考進來才呼叫),執行10萬次所需時間大約在0.35-0.45秒之間,與上方差異不大:

 


            DateTime tStart = DateTime.Now;
            object tResult = null;
            MyClass tClass = new MyClass();
            for (int i = 0; i < 100000; i++)
            {
                tClass.ReflectionTest();
            }
            DateTime tEnd = DateTime.Now;

            textBox4.Text = tEnd.Subtract(tStart).ToString();

透過Reflection呼叫同一個Assembly中的function,執行10萬次所需時間大約在0.7-0.8秒之間,大約是一般執行的兩倍:


            DateTime tStart = DateTime.Now;
            for (int i = 0; i < 100000; i++)
            {
                this.GetType().GetMethod("ReflectionTest").Invoke(this, null);
            }
            DateTime tEnd = DateTime.Now;

            textBox3.Text = tEnd.Subtract(tStart).ToString();

透過Reflection呼叫不同Assembly中的function,執行10萬次所需時間大約在1.5-1.6秒之間,大約是一般Reflection執行的兩倍:


            DateTime tStart = DateTime.Now;
            object tResult=null;
            Assembly tAss = Assembly.LoadFile(@"C:\ClassLibrary1.dll");
            for (int i = 0; i < 100000; i++)
            {
                Type tTypeOfClass = tAss.GetType("ClassLibrary1.MyClass");
                MethodInfo tRunMethod = tTypeOfClass.GetMethod("ReflectionTest");

                object tRunInstance = Activator.CreateInstance(tTypeOfClass);
                tResult = tRunMethod.Invoke(tRunInstance, null);
            }
            DateTime tEnd = DateTime.Now;

            textBox2.Text = tEnd.Subtract(tStart).ToString();

而如果我把Assembly.LoadFile搬到for迴圈中,執行1000次的時間大約就是1-1.1秒左右了,可見LoadFile的執行速度挺慢的。

Reflection真的很慢嗎?從以上的測試我們發現,慢歸慢,但不致於慢到離譜,起碼觸發了10萬次,秒數也才比一般執行慢1秒鐘,除以10萬次下來,每次多花的時間實在微不足道了,如果再一次的程式執行中,呼叫了10次的Reflection,看起來似乎也感覺不出來有太大的差異。

透過Reflection,我們就有機會做到下圖的結構,讀取XML中的定義,然後呼叫對應的Assembly,執行對應的function,藉以達到由XML中直接控制程式的運作的目的,不過要做到這一點,高度模組化絕對是一大要項,不夠模組化的Assembly,內容切的不夠仔細,如何供外部靈活調用。

image

Reflection的問題眾說紛紜,如何用的好且用的巧,我想還是值得大家去思考的,以上是一些思路,不代表我已經將此功能導入產品中,這必須要經過更仔細的測試與討論才能決定的。

而高度客製化能力與程式執行效率,孰輕孰重,相信大家自有見解了,也歡迎各位提供不同的想法。

 

參考資料:

 

游舒帆 (gipi)

探索原力Co-founder,曾任TutorABC協理與鼎新電腦總監,並曾獲選兩屆微軟最有價值專家 ( MVP ),離開職場後創辦探索原力,致力於協助青少年培養面對未來的能力。認為教育與組織育才其實息息相關,都是在為未來儲備能量,2018年起成立為期一年的專題課程《職涯躍升的關鍵24堂課》,為培養台灣未來的領袖而努力。