C#插件构架实战 http://blog.csdn.net/jery_lee/archive/2004/08/01/57951.aspx
C#插件构架实战 | |
Keywords C# 插件 反射(System.Reflection) 属性(System.Attribute) 一、引言 1. 问题的引入 假设你设计的程序已经部署到用户的计算机上,并且能够正常运行了。但是有一天,用户打来了电话——他们要求增加新的功能。确定了用户的需求后,你竟然发现原有的软件架构已经无法胜任新增任务的需求——你需要重新设计这个应用了!但问题是,就算你又用了一个开发周期完成了用户需要的应用,却不能保证用户的需求不会再次变更。也就是说,需求蔓延的可能性依然存在。因此,这种情况下插件构架更能显示出它的优越性。 2. 几个解决方案的对比 我总结了一下我所接触到的插件构架,大致上可分为以下几类: i> 脚本式 使用某种语言把插件的程序逻辑写成脚本代码。而这种语言可以是 Python ,或是其他现存的已经经过用户长时间考验的脚本语言。甚至,你可以自行设计一种脚本语言来配合你程序的特殊需要。当然,用当今最流行的 XML 是再合适不过了。 这种形式的特点在于,稍有点编程知识的用户就可以自行修改你的脚本( ^_^ 假如你不加密它的话)。我们无法论证这是好处还是坏处。因为,这种情况所造成的后果是不可预知的。 ii> 动态函数库 DLL 插件功能以动态库函数的形式存在。主程序通过某种渠道(插件编写者或某些工具)获得插件 DLL 中的函数签名,然后在合适的地方调用它们。用过 Matlab 的读者都知道, Matlab 中的各项功能几乎都是些动态链入的函数。 iii> 聚合式 顾名思义,就是把插件功能直接写成 EXE 。主程序除了完成自己的职责外,还负责调度这些“插件”。我不喜欢这种形式。这使插件与插件之间,主程序与插件之间(主要是这一点)的信息交流困难了许多。巴比伦塔的失败 [1] 从某种程度上讲就是信息交流无法实现造成的。 iv> COM 组件 COM [2] 的产生给这个世界增添了几分活力。只有接口!我们的插件需要做的只是实现程序定义的接口。主程序不需要知道插件怎样实现预定的功能,它只需要通过接口访问插件,并提供主程序相关对象的接口。这样一来,主程序与各插件之间的信息交流就变得异常简单。并且,插件对于主程序来说是完全透明的。 3. 决策 C# 是面向对象的程序设计语言。它提供了 interface 关键字来直接定义接口。同时, System.Reflection 命名空间也提供了访问外部程序集的一系列相关对象。这就为我们在 C# 中实现插件构架打下了坚实的基础。 下面,我们将以一个具有插件构架的程序编辑器为例,来阐述这种构架在 C# 中的实现。 二、设计过程 好了,现在我们准备把所有的核心代码都放在 CSPluginKernel 命名空间中。用VSIDE建立一个C#类库工程。在命名空间 CSPluginKernel 中开始我们的代码。 1. 接口设计 我们的程序编辑器会向插件开放正在编辑的文档对象。程序启动后,就枚举每一个插件并把它连接到主程序,同时传递主程序对象的接口。插件可以通过这个接口来请求主程序对象或访问主程序功能 。 根据上面的需求,我们首先需要一个主程序接口: public interface IApplicationObject { void Alert( string msg ); // 产生一条信息 void ShowInStatusBar( string msg ); // 将指定的信息显示在状态栏 IDocumentObject QueryCurrentDocument(); // 获取当前使用的文档对象 IDocumentObject[] QueryDocuments(); // 获取所有的文档对象 // 设置事件处理器 void SetDelegate( Delegates whichOne , EventHandler targer ); } // 目前只需要这一个事件 public enum Delegates { Delegate_ActiveDocumentChanged , } 然后是 IDocumentObject 接口。插件通过这个接口访问编辑器对象。 /// /// 编辑器对象必须实现这个接口 /// public interface IDocumentObject { // 这些属性是 RichTextBox 控件的相应的属性映射 string SelectionText { get ; set ; } Color SelectionColor { get ; set ; } Font SelectionFont { get ; set ; } int SelectionStart { get ; set ; } int SelectionLength { get ; set ; } string SelectionRTF { get ; set ; } bool HasChanges { get ; } void Select( int start , int length ); void AppendText( string str ); void SaveFile( string fileName ); void SaveFile(); void OpenFile( string fileName ); void CloseFile(); } 这个接口不需要过多解释。这里我只实现了RichTextBox控件少数的几个方法,其他可能用得到的,读者自行添加即可。 再然后,根据插件在其生命周期里的行为,设计插件的接口。 /// /// 本程序的插件必须实现这个接口 /// public interface IPlugin { ConnectionResult Connect( IApplicationObject app ); void OnDestory(); void OnLoad(); void Run(); } /// /// 表示插件与主程序连接的结果 /// public enum ConnectionResult { Connection_Success , Connection_Failed } 主程序会首先调用 Connect() 方法,并传递 IApplicationObject 给插件。插件在这个过程中做一些初始化工作。然后,插件的 OnLoad() 方法被调用。在这之后,当主程序接收到调用插件的信号时(键盘、鼠标响应)就会调用插件的 Run() 方法来启动这个插件。程序结束时,调用其 OnDestory() 方法。这样,插件的生命才宣告结束。 2. 插件信息的存储与获取 一个插件需要有它的名称 、版本等信息。作为设计者的你,也一定要留下你的尊姓大名和个人网站等用来宣传自己。 C# 的新特性——属性, 就是一个很好的解决方案。因此我们定义一个从 System.Attribute 继承来的类 PluginInfoArrtibute : /// /// 用来指定一个插件的相关信息 /// public class PluginInfoAttribute : System.Attribute { /// /// Deprecated. Do not use. /// public PluginInfoAttribute() {} public PluginInfoAttribute( string name , string version , string author , string webpage , bool loadWhenStart ) { // 细节已略去 } public string Name { get { return _Name; } } public string Version { get { return _Version; } } public string Author { get { return _Author; } } public string Webpage { get { return _Webpage; } } public bool LoadWhenStart { get { return _LoadWhenStart; } } /// /// 用来存储一些有用的信息 /// public object Tag { get { return _Tag; } set { _Tag = value ; } } /// /// 用来存储序号 /// public int Index { get { return _Index; } set { _Index = value ; } } private string _Name = ""; private string _Version = ""; private string _Author = ""; private string _Webpage = ""; private object _Tag = null ; private int _Index = 0; // 暂时不会用 private bool _LoadWhenStart = true ; } 用这个类修饰你的插件,并让他实现 IPlugin 接口: /// /// My Pluging 1( Just for test ) /// [ PluginInfo( "My Pluging 1( Just for test )" , "1.0" , "Jack H Hansen" , "http://blog.csdn.net/matrix2003b" , true ) ] public class MyPlugin1 : IPlugin { public MyPlugin1() { } #region IPlugin 成员 // 细节已略去 #endregion private IApplicationObject _App; private IDocumentObject _CurDoc; } 3. 加载插件 现在就得用到 System.Refelction 命名空间了。程序在启动时会搜索 plugins 目录下的每一个文件。对于每一个文件,如果它是一个插件,就用 Assembly 对象加载它。然后枚举程序集中的每一个对象。判断一个程序集是否为我们的插件的方法是判断它是否直接或间接实现自 IPlugin。用下面的函数,传递从程序集枚举的对象的System.Type。 private bool IsValidPlugin( Type t ) { bool ret = false ; Type[] interfaces = t.GetInterfaces(); foreach ( Type theInterface in interfaces ) { if ( theInterface.FullName == "CSPluginKernel.IPlugin" ) { ret = true ; break ; } } return ret; } 若条件都满足,IsValidPlugin() 就会返回 true 。接着程序就会创建这个对象并把它存于一个 ArrayList 中。 plugins.Add( pluginAssembly.CreateInstance( plugingType.FullName ) ); 现在,你就可以撰写测试代码了。 三、源代码 由于篇幅所限,完整的源代码(包含测试用例)请在下面的链接下载。下载后请用 VS.NET2003 打开,重新生成解决方案即可(需要 .NET Framework 1.1)。测试用例是一个在 RichTextBox 控件里插入红色文本的插件。很简单,只作测试之用。 四、结语 That''s all! 有了这种插件构架,可怜的程序员们就再也不用为需求蔓延耗费心机了。另外,欢迎对本文以及本文的附加代码作出评价。还有,就是,常去我的 Blog 看看~~ ^_^ 注: [1] 巴比伦塔的失败 《人月神话》,Frederick P. Brooks Jr. 第 7 章 为什么巴比伦塔会失败 [2] COM 有关 COM/COM+ 的详细技术细节请参见《 Mastering COM and COM+ 》 , Ash Rofail , Yasser Shohoud. |
Visual C#插件构架实战补遗 http://blog.csdn.net/matrix2003b/archive/2004/07/31/57180.aspx
主题:Visual C#插件构架实战补遗
在软件开发的过程中,设计的过程往往比写代码的过程要难得多。因此,通常除了软件测试之外,耗时最多的也就是系统建模了。一个好的软件系统应当具有较高的稳定性(可靠性)、易操作性以及可扩展性支持,尤其是可扩展性。我认为,良好的可扩展性支持是一个软件团队在开发中变被动为主动的必要条件。对于一个应用,我们希望在用户增加需求时,我们能够用最少的时间、最少的人力来解决问题。当别人在用户快速增长的需求中忙得不可开交时(用户总是不能在第一次需求分析时将需求完完整整的告诉你),而你,你的团队只需要作一点工作就可以让“贪得无厌”的用户得到满足,从而提高了效率,让团队有更多的的时间来创造,而不是去做无谓的修改。
很遗憾的是,在《C#插件构架实战》一文中,我并未考虑到这一点。当然,对于一个十八岁的没有也不可能有团队工作经验的年轻人来说,这样的失误(失误就是失败——老师如是说)是可以原谅的(自我开脱之辞)。不过,我决定对这个插件系统进行重构。
考虑到系统的复杂性,这次我准备使用UML(大上个月才开始学的,画得不好,见笑了)。
1. 着手分析
对于网友 jan 的指教,我大概明了,但人的思维差别太大,我不敢保证我的理解是完全符合 jan 的意思的。但是,我仍然会根据自己对可扩展性的理解构建一个应用程序框架模型。
直入正题。我现在假设我属于一个软件团队(就暂且叫她 AbstractSoft 吧),并任系统分析师。任何事物都有它规范的一面,我们希望我们的团队出品的部署在同一平台的所有应用都有相同的框架,相同的部署形式。这样便可以形成独有的团队特色,并在竞争中以效率取胜。因为我们不需要为每一套应用设计不同的框架——这可以节约不少时间!
这样我需要把程序实现与用户界面分开到不同的框架中。我的意思是:
|
如此一来,在 Application Frame Level 的核心库中存在的是抽象接口以及一些泛化的细节。这些内容在第一次安装团队产品时就已经部署在用户的机器上了。它不会自动销毁,直到用户提交把它从本地移除的请求。GUI Level 提供了团队产品泛化后的统一的界面组件(比如:属性编辑器、数据库操作界面等可重用组件)。特化的产品(Speciallized Application)通过实现 Application Frame Level 中的某些接口实现可扩展性,通过使用 GUI Level 中的的类来实现用户界面。
以下是一个简单的静态图(接口和类的成员将在下面详细阐述):
|
2. IConnectableObject
public interface IConnectable { // application 为插件所属的主框架对象。若为null则表示插件本身就是主框架 ConnectionResult Connect( object application ); ExtendibleVersionInfo VersionInfo { get; } void OnDestory(); void OnLoad(); void Run(); } public enum ConnectionResult { Connection_Success , Connection_Failed } public class ExtendibleVersionInfo { private ExtendibleVersionInfo() {} public ExtendibleVersionInfo( string name , string version , string copyright ) { // Omitted } public ExtendibleVersionInfo(string name,int version1,int version2,int version3,string copyright) { // Omitted } public int PrimaryVersion { get { return _Version1; } } public int SecondaryVersion { get { return _Version2; } } public int BuildVersion { get { return _Version3; } } public string Name { get { return _Name; } } public string VersionString { get { // Omitted } } public string Copyright { get { return _Copyright; } } private string _Name; private int _Version1 = 1; private int _Version2 = 0; private int _Version3 = 0; private string _Copyright; public static ExtendibleVersionInfo Empty = new ExtendibleVersionInfo(); } |
所有可连接的对象必须实现这个接口。这是所有 Application Frame Level 中类的鼻祖。
3. IExtendible
public interface IExtendible { IConnectable GetLatestVersion(); IConnectable QuerySpecifiedVersion( ExtendibleVersionInfo version ); ExtendibleVersionInfo[] EnumerateVersions(); } |
4. 使用类工厂创建应用程序和插件的最新版本
我们的主程序以及插件会设计成 internal class 。程序只输出一个工厂类,用户界面通过调用 IExtendible 接口的 GetLatestVersion() 方法获得这些用来完成实际任务的对象的实例,并把它们显示出来。或者,也可以枚举所有的版本,让用户来挑选所需要版本。
5. 可扩展性
不得不承认,这样的方式可扩展性仍不是很强。程序需要升级时同时需要修改提供给用户的工厂类(虽然接口不变)。为了实现更好的可扩展性,可以把简单工厂模式转换为工厂方法模式。
( Visual C#插件构架实战补遗 The End)