C# 插件构架实战

简介: 一、引言      1. 问题的引入     假设你设计的程序已经部署到用户的计算机上,并且能够正常运行了。但是有一天,用户打来了电话——他们要求增加新的功能。确定了用户的需求后,你竟然发现原有的软件架构已经无法胜任新增任务的需求——你需要重新设计这个应用了!但问题是,就算你又用了一个开发周期完成了用户需要的应用,却不能保证用户的需求不会再次变更。
一、引言

    
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));

     现在,你就可以撰写测试代码了。


三、源代码

     由于篇幅所限,完整的源代码(包含测试用例)请在下面的链接下载。
http://www.pscode.com/Upload_PSC/ftp/C__Plugin_1774987282004.zip
     下载后请用 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.
目录
相关文章
|
3月前
|
数据采集 JavaScript C#
C#图像爬虫实战:从Walmart网站下载图片
C#图像爬虫实战:从Walmart网站下载图片
|
4月前
|
数据采集 存储 C#
C# 爬虫技术:京东视频内容抓取的实战案例分析
C# 爬虫技术:京东视频内容抓取的实战案例分析
|
25天前
|
测试技术 Go C#
C#一分钟浅谈:ReSharper 插件增强开发效率
【10月更文挑战第25天】ReSharper 是 JetBrains 开发的一款 Visual Studio 插件,旨在提高 .NET 开发者的生产力。它通过代码分析、重构、导航等功能,帮助开发者避免常见错误,提升代码质量和开发效率。本文将通过具体代码案例,详细介绍 ReSharper 的常见功能及其应用。
37 1
|
2月前
|
开发框架 NoSQL MongoDB
C#/.NET/.NET Core开发实战教程集合
C#/.NET/.NET Core开发实战教程集合
|
3月前
|
设计模式 C# 开发者
C#设计模式入门实战教程
C#设计模式入门实战教程
|
5月前
|
算法 C#
C#实战 | 求解《九章算术》盈不足之共买物
【7月更文挑战第8天】中国古代数学成就显著,《九章算术》展示了先进的算法,如分数运算和方程解法,领先世界数百年。项目示例通过控制台应用,运用for循环和if条件语句,模拟解决书中盈不足问题,展示了解决数学问题的编程方法。
43 6
C#实战 | 求解《九章算术》盈不足之共买物
|
5月前
|
算法 C#
C#实战 | 求解《丘建算经》百鸡问题
【7月更文挑战第9天】《丘建算经》的百鸡问题是一个经典的不定方程问题,用C#解决时,通过三重嵌套循环穷举公鸡、母鸡和小鸡的组合。代码示例中,外层循环分别对应公鸡和母鸡,而小鸡数量由总钱数和已知鸡种计算得出,避免了额外的内层循环。使用`if`判断确保总数量正确。注意,除法运算可能导致整数截断错误,需使用3.0保证浮点数除法的准确性。这种方法虽然效率较低,但能确保找到所有可行解。
47 1
C#实战 | 求解《丘建算经》百鸡问题
|
4月前
|
数据安全/隐私保护 C# UED
利用 Xamarin 开展企业级移动应用开发:从用户登录到客户管理,全面演示C#与Xamarin.Forms构建跨平台CRM应用的实战技巧与代码示例
【8月更文挑战第31天】利用 Xamarin 进行企业级移动应用开发能显著提升效率并确保高质量和高性能。Xamarin 的跨平台特性使得开发者可以通过单一的 C# 代码库构建 iOS、Android 和 Windows 应用,帮助企业快速推出产品并保持一致的用户体验。本文通过一个简单的 CRM 示例应用演示 Xamarin 的使用方法,并提供了具体的代码示例。该应用包括用户登录、客户列表显示和添加新客户等功能。此外,还介绍了如何增强应用的安全性、数据持久化、性能优化及可扩展性,从而构建出功能全面且体验良好的移动应用。
53 0
|
4月前
|
前端开发 开发者 Apache
揭秘Apache Wicket项目结构:如何打造Web应用的钢铁长城,告别混乱代码!
【8月更文挑战第31天】Apache Wicket凭借其组件化设计深受Java Web开发者青睐。本文详细解析了Wicket项目结构,帮助你构建可维护的大型Web应用。通过示例展示了如何使用Maven管理依赖,并组织页面、组件及业务逻辑,确保代码清晰易懂。Wicket提供的页面继承、组件重用等功能进一步增强了项目的可维护性和扩展性。掌握这些技巧,能够显著提升开发效率,构建更稳定的Web应用。
105 0
|
4月前
|
前端开发 程序员 API
从后端到前端的无缝切换:一名C#程序员如何借助Blazor技术实现全栈开发的梦想——深入解析Blazor框架下的Web应用构建之旅,附带实战代码示例与项目配置技巧揭露
【8月更文挑战第31天】本文通过详细步骤和代码示例,介绍了如何利用 Blazor 构建全栈 Web 应用。从创建新的 Blazor WebAssembly 项目开始,逐步演示了前后端分离的服务架构设计,包括 REST API 的设置及 Blazor 组件的数据展示。通过整合前后端逻辑,C# 开发者能够在统一环境中实现高效且一致的全栈开发。Blazor 的引入不仅简化了 Web 应用开发流程,还为习惯于后端开发的程序员提供了进入前端世界的桥梁。
386 0