.NET插件系統(三) 插件間通信問題——設計可自組織和注入的組裝程序


一.  問題的背景

       動態系統的要求之一,是不同模塊可以根據自身需求自動組裝,這往往通過配置文件或用戶選擇進行。  這個基本問題在前面的文章中已經講述過了。

       但新的問題來了,我們定義了不同的插件A,B,C,那么,不同插件之間的通信如何進行?

   如果系統本身的框架非常明晰而且不易更改,那么面向固定接口的方法是最簡單方便的。 這也是大部分插件系統在“主結構”上使用的做法。

   但是,如果系統框架本身非常易變,連他們之間交互的接口都會隨着問題的不同而不同。這就好像,系統中包含不同種類的插座和插頭,我們需要自動將符合要求的插座和插頭安裝好,實現自動組網。如何實現這種自組織的組裝程序呢?

       

二 . 具體的案例

      為了便於更好的說明問題,以一個我實際面對的設計問題進行分析,實在抱歉由於時間所限不能提供Demo.

      我需要開發一個數據挖掘和處理平台,不同的功能通過插件的形式接入系統,並形成如左邊的可執行列表:

     

      用戶可以很簡單的通過拖拽,將左邊的處理拖到后邊的算法執行列表中,當點擊"運行"按鈕時,列表中的算法模塊會被順序或並行執行。

      這些算法是多樣的,比如數據挖掘中常用的  數據篩選,分詞,聚類,數據分類顯示等功能。你可以不必了解這些算法本身是什么,但本文中,您可能需要了解他們之間的結果可能相互依賴。這些算法的共同特征,是必須依賴於前一個或多個算法的結果,同時本身還可以輸出給其他算法。 例如,數據分類顯示必須依賴於聚類的結果,聚類則必須依賴於前期分詞和數據篩選的功能。

      這種結構很像順序流動的數據流,或者像電網或自來水網的結構。那么問題來了,系統執行前不知道這些算法到底是什么,那怎么能提供插件間交互的需求?  一種做法是,讀寫數據庫,只要上一個算法告訴下一個算法數據的位置在哪里就可以了,但這種做法很不“環保”,試想,好好的存在內存中的數據,干嘛要寫到硬盤中再讀出來呢?這會造成無謂的開銷。

      另外,算法執行列表(右邊)的順序應該與組裝順序無關,意思是處在數據流上游的模塊不一定就在執行列表的上游。

    我們必須設計一套方法,能實現這些算法的相互通信。

    

 三 . 聲明可提供接口和注入接口需求

       首先,為了保證重用,算法模塊之間的通信方式只能是接口或抽象類。不論如何,算法應該告訴管理器,它必須依賴什么,它可以提供什么。

       如果一個算法模塊可以提供某接口的結果,那么它必須實現該接口。

   如果算法必須依賴某接口,那么它應該最少包含一個該接口的內部成員,或者,也實現之(本文沒有考慮這種情況)。

       下面我們簡單實現兩個類:

        計算方法A可以輸出接口B和C,但計算方法B必須得到兩個接口B和C的結果。

 1  [SelfBuildClassAttribute(new string[] { }, new string[] { "IB", "IC" })]
2 [XFrmWorkAttribute("計算方法A", "IDataProcess", "可輸出接口B和C", "123")]
3 public class Test1 : AbstractProcessMethod, IC, IB
4 {
5 public string outputC
6 {
7 get;
8 set;
9 }
10
11 public string outputB
12 {
13 get;
14 set;
15 }
16 public override bool DataProcess()
17 {
18 outputC = "已經正確賦值C";
19 outputB = "已經正確賦值B";
20 return true;
21 }
22
23 }
24
25
26
27
28 [SelfBuildClassAttribute(new string[] { "IB", "IC" }, new string[] { })]
29 [XFrmWorkAttribute("計算方法B", "IDataProcess", "必須通過外界提供B和C的接口", "123")]
30 public class Test2 : AbstractProcessMethod
31 {
32 [SelfBuildMemberAttribute("IB")]
33 public IB calledIB { get; set; }
34
35 [SelfBuildMemberAttribute("IC")]
36 public IC callIC { get; set; }
37 public override bool DataProcess()
38 {
39
40 XLogSys.Print.Debug(calledIB.outputB);
41 XLogSys.Print.Debug(callIC.outputC);
42 return true;
43 }
44 }

      兩個方法方法非常簡單,繼承於AbstractProcessMethod類,你不需要關心這個類的具體內容,只需注意 Test1實現了兩個接口IB和IC,這兩個接口都能提供兩個字符串。Test2類則必須獲得IB和IC兩個接口的字符串成員。

       我們可以通過自定義Attribute實現可提供和依賴的接口的標識。 本系統中使用了兩個自定義的attribute:

  • [SelfBuildClassAttribute(new string[] { }, new string[] { "IB", "IC" })]
    兩個必選形參,即需求的接口字符串列表 和 提供的接口字符串列表。
  • [SelfBuildMemberAttribute("IC")]
    要求被注入的需求者成員變量標識

        (XFrmWorkAttribute是插件的標記,詳情可見我上一篇關於插件的文章)     

 1  /// <summary>
2 /// 實現自組織算法的特性,它一般標記在模塊的類名之前
3 /// </summary>
4 public class SelfBuildClassAttribute:Attribute
5 {
6 /// <summary>
7 /// 要求的依賴項接口
8 /// </summary>
9 public ICollection<string> dependInterfaceCollection
10 {
11 get;
12 set;
13 }
14 /// <summary>
15 /// 可以輸出的接口
16 /// </summary>
17 public ICollection<string> outputInterfaceCollection
18 {
19 get;
20 set;
21 }
22 public SelfBuildClassAttribute(string[] dependInterfaceName, string[] outputInterfaceName)
23 {
24 dependInterfaceCollection = new List<string>();
25 outputInterfaceCollection = new List<string>();
26 foreach (string rc in dependInterfaceName)
27 {
28 dependInterfaceCollection.Add(rc);
29 }
30 foreach (string rc in outputInterfaceName)
31 {
32 outputInterfaceCollection.Add(rc);
33 }
34 }
35 public SelfBuildClassAttribute(ICollection<string> dependInterface, ICollection<string> outputInterfaceName)
36 {
37 dependInterfaceCollection = dependInterface;
38 outputInterfaceCollection = outputInterfaceName;
39 }
40 }
41
42
43 /// <summary>
44 /// 自組織成員特性,一般放置在類的 要求注入的成員名上
45 /// </summary>
46 public class SelfBuildMemberAttribute:Attribute
47 {
48         public string invokeName{get;set;}  //需要被注入的依賴項接口
49 public SelfBuildMemberAttribute(string myInvokeName)
50 {
51 invokeName = myInvokeName;
52 }
53 }

        這兩個類的作用已經在注釋上寫清楚了,您可以結合Test1和Test2兩個類的具體實現來理解:  Test1不需要依賴任何接口,但可以輸出兩個接口IB,IC。  Test2方法需要依賴IB和IC兩個接口,因此它有兩個成員變量,並加上了標記,標記的內容是該接口的名稱。

  下面,我們要做的工作,就是在運行時,自動將test1的方法注入到Test2的內部接口上。

 

四 .  實現內部組裝

        當用戶點擊運行時,系統會自動實現接口裝配,並按照執行策略執行列表當中的算法模塊。

       

 /// <summary>
/// 可實現自組織的算法模塊設計
/// </summary>
/// <typeparam name="T"></typeparam>
public class SelfBuildProcessMethodCollection<T>:ProcessMethodCollection<T> where T:IProcess
{


SelfBuildManager mySelfBuildManager = new SelfBuildManager();
/// <summary>
/// 是否設置自動裝配算法模塊
/// </summary>
public bool isSelftBuild = true;
protected override bool BeginProcess()
{

mySelfBuildManager.BuildModule(this); //實現接口的自動裝配
base.BeginProcess();


return true;


}
}

      在我的系統中,所有算法的抽象接口都是Iprocess,但在這篇文章中,自組織並不一定需要該接口。系統保存的算法保存在了ICollection<IProcess>接口中。而具體裝配的方法,則定義在SelfBuildManager中。

    public class SelfBuildManager
{
/// <summary>
/// 保存所有依賴接口的字典
/// </summary>
Dictionary<string, IProcess> dependDictinary = new Dictionary<string, IProcess>();
/// <summary>
/// 可提供接口的集合字典
/// </summary>
Dictionary<string, IProcess> outputDictinary = new Dictionary<string, IProcess>();
public void BuildModule(ICollection<IProcess> processCollection)
{
myProcessCollection = processCollection;
GetAllDependExportDictionary(); //獲取所有依賴和能提供的接口字典
BuildAttribute(); //實現接口自動組裝的方法
}

ICollection<IProcess> myProcessCollection;
private void GetAllDependExportDictionary( )
{
foreach (IProcess rc in myProcessCollection)
{
Type type = rc.GetType();
// Iterate through all the Attributes for each method.
foreach (Attribute attr in
type.GetCustomAttributes(typeof(SelfBuildClassAttribute), false))
{
SelfBuildClassAttribute attr2 = attr as SelfBuildClassAttribute;
foreach (string outputString in attr2.outputInterfaceCollection)
{
outputDictinary.Add(outputString, rc);
}
foreach (string dependString in attr2.dependInterfaceCollection)
{
dependDictinary.Add(dependString, rc);
}
}
}
}


private void BuildAttribute()
{
foreach (KeyValuePair<string, IProcess> dependNeeder in dependDictinary)
{
IProcess outputProvider;
outputDictinary.TryGetValue(dependNeeder.Key,out outputProvider); //在輸出字典中找到滿足該依賴項的接口
if (outputProvider != null)
{
PropertyInfo[] PropertyInfoArray=dependNeeder.Value.GetType().GetProperties(); ///獲取該類的所有屬性列表
foreach (PropertyInfo fl in PropertyInfoArray)
{
foreach (Attribute attr in fl.GetCustomAttributes(typeof(SelfBuildMemberAttribute), false))
{
SelfBuildMemberAttribute attr2 = attr as SelfBuildMemberAttribute; //找到自裝配成員的標記
if (attr2 != null && attr2.invokeName == dependNeeder.Key)
{
try
{

fl.SetValue(dependNeeder.Value, outputProvider, null); //通過反射,將提供者注入到需求者的變量中
}
catch (System.Exception ex)
{
XLogSys.Print.Error(ex.Message+"無法進行組裝");
}

break;

}
}

}

}



}

}
}

        具體的方法請參考代碼的注釋部分。由於代碼注釋已經很詳細了,因此不做更多解釋。

 

五. 實現和驗證

     我們將計算方法A和計算方法B都拖入算法執行列表中:

   

     並單擊執行按鈕:

     

      可以看到,接口確實被正確賦值了。設計成功。

六. 必須考慮的問題和擴展點

     雖然設計成功,但系統有一些不可避免的問題:

  1. 如果一個需求者發現有不止一個滿足該需求的提供者,那么如何選擇?目前系統未作此區分,僅僅在找到第一個適配對象后停止搜索。合適的方法是提供用戶介入的控制方案,即用戶可以用線將不同算法的需求和提供聯系起來,當然,該需求暫時有些復雜,如果作者實現了它,一定會公開其方法。
  2. 性能和靈活性: 通過反射實現的方法必須討論性能,好在系統只執行一次裝配過程,並盡可能的通過標記簡化搜索條件。  但應該研究更好的搜索方法。
  3. 該功能的易用性: 作者本人認為該系統是足夠易用的,你可以簡單地將需求和提供接口的字符串列表標記在類前,並將需求的接口標記在需求方的成員變量前,暫時沒有想到更好的做法。
  4. 相互依賴問題:一種可能的情況是算法A依賴算法B的結果,算法B依賴A的結果,這種情況一定是不允許的嗎?不一定,但若能處理這種需求,就可能實現更強的靈活性,同時帶來更復雜的組裝邏輯。

   有任何問題,歡迎討論!

  

 

         

      

  

      

       

      

      

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM