插件模型是指應用程式由多個動態、獨立的模組組成,每個模組擁有一或多個服務,並遵循特定的插件協議,透過主程式實現主程式與插件、插件與插件之間的通訊。其核心在於定義一套公共介面,利用反射機制獲取相關屬性與方法,進而執行指定操作。換句話說,這是一種定義通用解決方案的模式,透過反射取得組件的相關型別並執行操作,具備即插即用、更新維護簡便的特點。
本文將介紹一個簡單的插件開發模型,主要步驟包含三大階段:定義公共介面與抽象類別、實作相關組件、以及建立通用組件反射操作類別。
在這個模型中,公共介面與抽象類別定義於 Jasen.Framework.Core 組件,其中包含通用的組件反射操作類別 AssemblyUtility。具體實作的組件則分別為 Jasen.Framework.Oracle、Jasen.Framework.Access 與 Jasen.Framework.SqlServer,它們皆實作核心組件中的公共介面。客戶端可根據實際需求動態載入對應組件。
首先定義公共介面 IDataTable 與抽象類別 DataTable,置於最上層組件。其他組件分別建立專案,實作對應功能,如 SqlServerDataTable、OracleDataTable、AccessDataTable。特別注意:使用 Assembly.LoadFile 動態載入組件時,該組件在當前執行環境中必須不存在,否則可能導致資料異常,因此各實作組件必須獨立(僅實作公共介面)。
AssemblyUtility 是操作組件的通用類別,支援依指定目錄或檔案清單動態載入組件,並能取得型別集合與物件實例集合。關鍵在於實作的子類別必須提供預設建構函式。以下為核心程式碼:
public static class AssemblyUtility
{
public static IEnumerable<Type> GetImplementdTypesByDirectory<T>(string baseDirectory)
{
IList<Assembly> assemblies = GetAssemblies(baseDirectory);
List<Type> types = new List<Type>();
foreach (Assembly assembly in assemblies)
{
types.AddRange(GetImplementdTypes<T>(assembly));
}
return types;
}
public static IEnumerable<Type> GetImplementdTypes<T>(string assemblyFile)
{
if (!File.Exists(assemblyFile)) return null;
try { return GetImplementdTypes<T>(Assembly.LoadFile(assemblyFile)); }
catch { return null; }
}
public static IEnumerable<Type> GetImplementdTypes<T>(Assembly assembly)
{
if (assembly == null) return null;
return assembly.GetExportedTypes()
.Where(p => p.IsSubclassOf(typeof(T)) && !p.IsAbstract && !p.IsInterface);
}
public static IList<T> GetImplementedObjectsByDirectory<T>(string baseDirectory)
{
IList<Assembly> assemblies = GetAssemblies(baseDirectory);
List<T> entities = new List<T>();
foreach (Assembly assembly in assemblies)
{
entities.AddRange(GetImplementedObjects<T>(assembly));
}
return entities;
}
public static IList<T> GetImplementedObjects<T>(string assemblyFile)
{
if (!File.Exists(assemblyFile)) return null;
try { return GetImplementedObjects<T>(Assembly.LoadFile(assemblyFile)); }
catch { return null; }
}
public static IList<T> GetImplementedObjects<T>(Assembly assembly)
{
if (assembly == null) return null;
IEnumerable<Type> types = GetImplementdTypes<T>(assembly);
var result = new List<T>();
foreach (Type type in types)
{
ConstructorInfo constructor = type.GetConstructor(new Type[0]);
if (constructor == null) continue;
object instance = Activator.CreateInstance(type);
if (instance is T) result.Add((T)instance);
}
return result;
}
public static IList<Assembly> GetAssemblies(string baseDirectory)
{
if (!Directory.Exists(baseDirectory)) return new List<Assembly>();
string[] files = Directory.GetFiles(baseDirectory, "*.dll");
return GetAssemblies(files);
}
public static IList<Assembly> GetAssemblies(string[] assemblyFiles)
{
IList<Assembly> assemblies = new List<Assembly>();
try
{
foreach (string file in assemblyFiles)
{
if (!File.Exists(file) || !file.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase)) continue;
assemblies.Add(Assembly.LoadFile(file));
}
}
catch { return new List<Assembly>(); }
return assemblies;
}
}
GetImplementdTypesByDirectory、GetImplementdTypes (依檔案/組件) — 依參數動態取得指定基底類別 T 的非抽象、非介面子類別型別。GetImplementedObjectsByDirectory、GetImplementedObjects (依檔案/組件) — 同樣依參數動態建立型別 T 的實例。若子類別有參數建構函式,必須實作無參數建構函式,否則略過該型別。測試涵蓋組件載入、型別與物件取得等情境,確保 AssemblyUtility 運作正確。範例測試碼如下:
[TestMethod]
public void GetImplementedObjectsByDirectoryTest()
{
string assemblyDir = AppDomain.CurrentDomain.BaseDirectory + "\\Files\\";
IList<DataTable> result = AssemblyUtility.GetImplementedObjectsByDirectory<DataTable>(assemblyDir);
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Count);
}
本文以 AssemblyUtility 為核心示範插件開發思路。實際應用時,客戶端只需取得型別或物件集合,再執行具體操作。插件組件放置於指定目錄,透過 AssemblyUtility 動態載入即可取得所需型別資料。具體功能邏輯封裝於各插件組件中,主程式無需知悉細節,達到高度解耦。
原始碼下載:C# 插件開發模型原始碼