如何构建理论框架一个插件框架

阅读提示:这篇文章将讲述如何利用C#奇妙的特性,实现插件架构,用插件(plug-ins)机制建立可扩展的解决方案。
在.NET框架下的C#语言,和其他.NET语言一样提供了很多强大的特性和机制。其中一些是全新的,而有些则是从以前的语言和平台上照搬过来的。然而,这种巧妙的结合产生了一些有趣的方法可以用来解决我们的问题。这篇文章将讲述如何利用这些奇妙的特性,用插件(plug-ins)机制建立可扩展的解决方案。后面也将提供一个简要的例子,你甚至可以用这个东西来替换那些已经在很多系统中广泛使用的独立的程序。在一个系统中,可能有很多程序经常需要进行数据处理。可能其中有一个程序用于处理雇员的信息,而另一个用来管理客户关系。
在大多数情况下,系统总是被设计为很多个独立的程序,他们之间很少有交互,经常使用复制代码的办法来共享。而实际上这样的情况可以把那些程序设计为插件,再用一个单一的程序来管理这些插件。这种设计可以让我们更好的在不同的解决方案中共享公用的方法,提供统一的感观。
图片一是一个例子程序的截图。用户界面和其他常见的程序没有什么不同。整个窗体被垂直的分割为两块。左边的窗格是个树形菜单,用于显示插件列表,在每个插件的分支下面,列出了这个插件所管理的数据。而右边的窗格则用于编辑左边被选中的插件的数据。各个插件提供各自的编辑数据的界面。图片一展示了一个精巧的工作区。
那么,主程序必须能够加载插件,然后和这些插件进行通信,这样才能实现我们的设计。所有这些的实现可以有很多不同的方法,仅取决于开发者选择的语言和平台。如果选择的是C#和.NET,那么反射(reflection)机制可以用来加载插件,并且其接口和抽象类可以用于和插件通信。
为了更好的理解主程序和插件之间的通信,可以先了解一下设计模式。设计模式最早由Erich Gamma提出1,它利用和对象思想来实现通用的通信模型。不管组件是否具有不同的输入和输出,只要他们有相似的结构。设计模式可以帮助开发者利用广受证明的面向对象理论来解决问题。事实上它就是描述解决方案的语言,而不用管问题的具体细节或者编程语言的细节。设计模式策略的关键点在于如何把整个解决方案根据功能来分解,这种分解是通过把主程序的不同功能分开执行而完成的。这样主程序和子程序之间的通信可以通过设计良好的接口来完成。通过这种分解我们立即可以得到这两个好处:第一,软件项目被分成较小的不相干的单位,工作流程的设计可以更容易,而较小的代码片断意味着代码更容易建立和维护。第二个好处在于改变程序行为的时候并不会关系到主程序的运行,主程序不用关心子程序如何,他们之间只要有通用的通讯机制就足够了。
在C#程序中,接口是用来定义一个类的功能的。接口定义了预期的方法,属性,事件信息。为了使用接口,每个具体的函数必须严格按照接口的定义完成所描述的功能。列表一展示了上面例子程序的接口:IPlug。这个接口定义了四个方法:GetData,GetEditControl,Save和Print。这四个定义并没有描述具体是怎么完成的,但是他们保证了这个类支持IPlug接口,也就是保证支持这些方法的调用。
在查看代码之前,讨论总是先得转移到属性定制上面.属性定制是.NET提供的一个非常棒的新特性之一,属性对于所有的编程语言都是一种通用的结构.举个例子,一个函数用于标识可访问权限的public,private,或者protect标志就是这个函数的一个属性.属性定制之所以如此让人兴奋,那是因为编程人员将不再只能从语言本身提供的有限的属性集中选择.一个定制的属性其实也是一个类,它从System.Attribute继承,它的代码被允许是自我描述的.属性定制可以应用于绝大多数结构中,包括C#里面的类,方法,事件,域和属性等等.示例代码片断定义了两个定制的属性:PlugDisplayNameAttribute和PlugDescriptionAttribute,所有的插件内部的类必须支持这两个属性.列表二是用于定义PlugDisplayNameAttribute的类.这个属性用于显示插件节点的内容.在程序运行的时候,主程序将可以利用反射(reflection)来取得属性值.
插件(Plug-Ins)
上面的示例程序包括了两个插件的执行.这些插件在EmployeePlug.cs和CustomerPlug.cs中定义.列表三展示了EmployeePlug类的部分定义.下面是一些关键点.
1.这个类实现了IPlug接口.由于主程序根本不会知道插件内部的类是如何定义的,这非常匾?主程序需要使用IPlug接口和各个插件通信.这种设计利用了面向对象概念里面的"多态性".多态性允许运行时,可以通过指向基类的引用,来调用实现派生类中的方法.2.这个类被两个属性标识,这样主程序可以判断这个插件是不是有效的.在C#中,要给一个类标识一个属性,你得在类的定义之前声明属性,内容附在括号内.3.简明起见,例子只是使用了直接写入代码的数据.而如果这个插件是个正式的产品,那么数据总是应该放在中或者文件中,各自所有的数据都应该仅仅由插件本身来管理.EmployeePlug类的数据在这里用EmployeeData对象来存储,那也是一个类型并且实现了IPlugData接口.IPlugData接口在IPlugData.cs中定义,它提供了最基础的数据交换功能,用于主程序和插件之间的通讯.所有支持IPlugData接口的对象在下层数据变化的时候将提供一个通知.这个通知实际上就是DataChanged事件的发生.4.当主程序需要显示某个插件所含数据列表的时候,它会调用GetData方法.这个方法返回IPlugData对象的一个数组.这样主程序就可以对数组中的每个对象使用ToString方法得到数据以建立树的各个节点.ToString方法是EmployeeData类的一个重载,用于显示雇员的名字.5.IPlug接口也定义了Save和Print方法.定义这两个方法的目的在于当有需要打印或者保存数据的时候,要通知一个插件.EmployeePlug类就是用于实现打印和保存数据的功能的.在使用Save方法的时候,需要保存数据的位置将会在方法调用的时候提供.这里假设主程序会向用户查询路径等信息.路径信息的查询是主程序提供给各个插件的服务.对于Print方法,主程序将把选项和内容传递到System.Drawing.Printing.PrintDocument类的实例.这两种情况下,和用户的交互操作都是一致的由主程序提供的.
反射(Reflection)
在一个插件定义好之后,下一步要做的就是查看主程序是怎么加载插件的.为了实现这个目标,主程序使用了反射机制.反射是.NET中用于运行时查看类型信息的.在反射机制的帮助下,类型信息将被加载和查看.这样就可以通过检查这个类型以判断插件是否有效.如果类型通过了检查,那么插件就可以被添加到主程序的界面中,就可以被用户操作.
示例程序使用了.NET框架的三个内置类来使用反射:System.Reflection.Assembly,System.Type,和System.Activator.
System.Reflection.Assembly类描述了.NET的程序集.在.NET中,程序集是配置单元.对于一个典型的Windows程序,程序集被配置为单一的Win32可执行文件,并且带有特定的附加信息,使之适应.NET运行环境.程序集也可以配置为Win32的DLL(动态链接库),同样需要带有.NET需要的附加信息.System.Reflection.Assembly类可以在运行的时候取得程序集的信息.这些信息包括程序集包含的类型信息.
System.Type类描述了类型定义.一个类型声明可以是一个类,接口,数组,结构体,或者枚举.在加载了一个类之后,System.Type类可以被用于枚举该类支持的方法,属性,事件和接口.
System.Activator类用于创建一个类的实例.
列表四展示了LoadPlugs方法.LoadPlugs方法在HostForm.cs中定义,是HostForm类的一个private的非静态方法.LoadPlugs方法使用.NET的反射机制来加载可用的插件文件,并且验证它们是否符合被主程序使用的要求,然后把它们添加到主程序的树形显示区中.这个方法包含了下面几个步骤:
1.通过使用System.IO.Directory类,我们的代码可以用通配符来查找所有的以.plug为扩展名的文件.而Directory类的静态方法GetFiles能够返回一个System.String类型的数组,以得到每个符合要求的文件的物理路径.2.在得到路径字符串数组之后,就可以开始把文件加载到System.Reflection.Assembly实例中了.建立Asdsembly对象的代码使用了try/catch代码块,这样如果某个文件并不是一个有效地.NET程序集,就会抛出异常,程序此时将弹出一个MessageBox对话框,告诉用户无法加载该文件.循环一直进行直到所有文件都已遍历完成.3.在一个程序集加载之后,代码将遍历所有可访问到的类型信息,检查是否支持了HostCommon.IPlug接口.4.如果所有类型都支持HostCommon.IPlug接口,那么代码继续验证这些类型,检查是否支持那些已预先为插件定义好的属性.如果没有支持,那么一个HostCommon.PlugNotValidException类型的异常将会被抛出,同样,主程序将会弹出一个MessageBox,告诉用户出错的具体信息.循环一直进行直到所有文件都已遍历完成.5.最后,如果这些类型支持HostCommon.IPlug接口,也已定义了所有需要定义的属性,那么它将被包装为一个PlugTreeNode实例.这个实例就会被添加到主程序的树形显示区.
主程序框架被设计为两个程序集.第一个程序集是Host.exe,它提供了主程序的Windows窗体界面.第二个程序集是HostCommon.dll,它提供了主程序和插件之间进行通信所需的所有类型定义.比如,IPlug接口就是在HostCommon.dll里面配置的,这样它可以被主程序和插件等价的访问.这两个程序集在一个文件夹内,同样的,附加的作为插件的程序集也需要被配置在一起.那些程序集被配置在plugs文件夹内(主程序目录的一个子文件夹).EmployeePlug类在Employee.plug程序集中定义,而CustomerPlug类在Customer.plug程序集中定义.这个例子指定插件文件以.plug为扩展名.事实上这些插件就是个普通的.NET类库文件,只是通常库文件使用.dll扩展名,这里用.plug罢了.特殊的扩展名对于程序运行是完全没有影响的,但是它可以让用户更明确的知道这是个插件文件.
设计的比较
并不是一定要像例子程序这样设计才算正确的.比如,在开发一个带有插件的C#程序时,并不一定需要使用属性.例子里使用了两个自定义的属性,其实也可以新定义两个IPlug接口的参数来实现.这里选择用属性,是因为插件的名字和它的描述在本质上确实就是一个事物的属性,符合规范.当然了,使用属性会造成主程序需要更多的关于反射的代码.对于不同的需求,设计者总是需要做出合理的决定.
示例程序被设计为尽量的简单,以帮助理解主程序和插件之间的通信.在实际做产品的时候,可以做很多的改进以满足实用要求.比如:
1.通过对IPlug接口增加更多的方法,属性,事件,可以增加主程序和插件之间的通信点.两者间的更多的交互操作使得插件可以做更多的事情.2.可以允许用户主动选择需要加载的插件.
源代码示例程序的完整的源代码可以在这里下载:
译者注:以前就考虑过在.NET里面如何实现插件机制,做来做去总是觉得设计上不够好.而昨天在网上无意中发现了这篇文章,写的实在是太棒了,所以看完之后,决定把它翻译过来,前后一共花了大概10个小时吧.翻译的可能不太好,请见谅.文中有什么错误,请不吝指正.
备注1 Erich Gamma et al. Design Patterns (Addison-Wesley, 1995).
列表一:The IPlug interface
public interface IPlug{IPlugData GetData();PlugDataEditControl GetEditControl(IPlugData Data);bool Save(string Path);bool Print(PrintDocument Document);}
列表二:The PlugDisplayNameAttribute class definition
AttributeUsage(AttributeTargets.Class)public class PlugDisplayNameAttribute : System.Attribute{private string _displayN
public PlugDisplayNameAttribute(string DisplayName) : base(){_displayName=DisplayN}
public override string ToString(){return _displayN}
列表三:A partial listing of the EmployeePlug class definition
PlugDisplayName("Employees")PlugDescription("This plug is for managing employee data")public class EmployeePlug : System.Object, IPlug{public IPlugData GetData(){IPlugData data = new EmployeeData{new EmployeeData("Jerry", "Seinfeld"),new EmployeeData("Bill", "Cosby"),new EmployeeData("Martin", "Lawrence")};
public PlugDataEditControl GetEditControl(IPlugData Data){return new EmployeeControl((EmployeeData)Data);}
public bool Save(string Path){//implementation not shown}
public bool Print(PrintDocument Document){//implementation not shown}}
列表四:The method LoadPlugs
private void LoadPlugs(){string files = Directory.GetFiles("Plugs", "*.plug");
foreach(string f in files){
try{Assembly a = Assembly.LoadFrom(f);System.Type types = a.GetTypes();foreach(System.Type type in types){if(type.GetInterface("IPlug")!=null){if(type.GetCustomAttributes(typeof(PlugDisplayNameAttribute),false).Length!=1)throw new PlugNotValidException(type,"PlugDisplayNameAttribute is not supported");if(type.GetCustomAttributes(typeof(PlugDescriptionAttribute),false).Length!=1)throw new PlugNotValidException(type, "PlugDescriptionAttribute is not supported");
_tree.Nodes.Add(new PlugTreeNode(type));}}}catch(Exception e){MessageBox.Show(e.Message);}}
关于作者Shawn Patrick Walcheske是美国Arizona州Phoenix市的一名软件开发工程师.他同时是Microsoft Certified Solution Developer和Sun Certified Programmer for the&&2 Platform.你可以在这里联系到他,.
原文作者:Shawn Patrick Walcheske
原文链接:
阅读(...) 评论()十大前端开发框架(上) - 文章 - 伯乐在线
& 十大前端开发框架(上)
, , , , , ,
编者按:考虑到英文原文的长度以及可读性,十大前端开发框架将分成上下两部分呈现给大家。上半部分着重讲的是Bootstrap家族框架,第二节将会跟大家分享更多其他的框架。
随着互联网的不断成熟以及我们越来越多的用各种移动端的设备访问互联网,Web设计师和Web开发者的工作也变得越来越复杂。
十年前,一切都还简单得多。那个时候,大部分用户都是坐在桌子前通过一个大大的显示器来浏览我们的网页。960像素是当时比较合理的网页宽度。那些年我们的开发工作主要就是跟十几个桌面浏览器打交道,并通过添加几个浏览器的hack,来兼容诡异的旧版本 IE 浏览器。时至今日,随着过去五六年间手持电子设备的突飞猛进,一切都变了样。我们看到各种尺寸的智能手机和平板层出不穷,电子阅读器,以及电视设备上的浏览器等也不断涌现。这种设备的多样性正在与日俱增。
可以预见,在不远的将来,相对于使用台式机,越来越多的人会使用移动设备来访问互联网。事实上,已经有相当数量的一部分人只通过智能手机上网。这意味着,我们这些Web设计师和开发者需要知道如何在庞大的移动端王国里呈现以及适配我们的产品,这至关重要。在撰写本文的时候,尽管我们还没彻底搞明白如何将桌面端呈现的全部内容在手持设备中呈现同样的效果,但是用于实现这一目标的技术以及工具正在变得越来越好。
在不知道浏览设备屏幕大小的时候,最主要的策略就是使用响应式网页设计。它是一种根据设备浏览窗口的尺寸大小来输出相应页面布局的方法。小型移动设备(如智能手机以及平板电脑)上的大多数浏览器会默认将一个网页缩小到适应自己的屏幕尺寸,然后用户可以通过缩放以及滚动等方法浏览整个网页。这种方法在技术上是可行的,但是从用户体验的角度上讲却比较糟糕。小屏幕上文字太小阅读不方便,链接太小难以点击,缩放以及滚动的操作多多少少会让人在阅读的时候分心。
响应式网页设计利用同样的HTML文档来适配所有的终端设备,响应式网页设计会根据设备屏幕的大小加载不同的样式,从而在不同的终端设备上呈现最优的网页布局。举个例子,当你在大屏幕桌面浏览器中查看一个网页的时候,网页的内容可能是分为很多列的,并且有常见的导航条。如果你在小屏幕的智能手机上查看同样的页面,你会发现页面的内容呈现在同一列中,并且导航按钮足够大,点击起来很方便。你可以在这个网站上看到很多响应式网页设计的案例。在你的浏览器中随便点开一个设计案例,然后改变浏览器窗口的大小,你会看到网页的布局会根据窗口大小相应变化。
到目前为止,我们可以看出,响应式网页设计可以有效地帮助我们应对日益增长的终端设备多样性。那么在我们设计网页的时候有哪些实际可用的工具以及技术可以用来实现响应式网页设计呢?我们每个人都需要成为web大师才能驾驭这门技术么?或者是利用我们已经掌握的web基本知识就已经足够了?目前有什么工具可以帮到我们么?
这时候前端开发框架华丽登场。响应式网页设计实现起来并不困难,但是要让它在所有的目标设备上都正常运作会有一点小棘手。框架可以让这一工作变得简单。利用框架,你可以花最少的力气创建响应式且符合标准的网站,一切都很简单并且具有一致性。使用框架有很多好处,比如说简单快速,以及在不同的设备之间的一致性等等。框架最大的优势就是简单易用,即使只掌握少量的web知识,你也可以毫无障碍的使用它们。
简而言之,如果你认真对待目前的web开发工作,那么使用框架进行开发就不是可选项而是必须要做的事情。你的站点必须高度灵活以适应不同的浏览器,平板,智能手机以及其他各种各样的手持设备。
一个前端开发框架其实就是一系列产品化的HTML/CSS/JavaScript组件的集合,我们可以在设计中使用它们。前端开发框架有很多,其中有一些写得很棒。为了大家的使用便利,下文列举了目前最强大应用最广泛的几款前端开发框架。记住,这些框架并不仅仅是CSS 栅格之类的一些东西,它们包括的是整套的前端开发框架。
1. Bootstrap
Boostrap绝对是目前最流行用得最广泛的一款框架。它是一套优美,直观并且给力的web设计工具包,可以用来开发跨浏览器兼容并且美观大气的页面。它提供了很多流行的样式简洁的UI组件,栅格系统以及一些常用的JavaScript插件。
Bootstrap是用动态语言LESS写的,主要包括四部分的内容:
脚手架——全局样式,响应式的12列栅格布局系统。记住Bootstrap在默认情况下并不包括响应式布局的功能。因此,如果你的设计需要实现响应式布局,那么你需要手动开启这项功能。
基础CSS——包括基础的HTML页面要素,比如表格(table),表单(form),按钮(button),以及图片(image),基础CSS为这些要素提供了优雅,一致的多种样式。
组件——收集了大量可以重用的组件,如下拉菜单(dropdowns),按钮组(button groups),导航面板(navigation control)——包括:tabs,pills,lists标签,面包屑导航(breadcrumbs)以及页码(pagination),缩略图(thumbnails),进度条(progress bars),媒体对象(media objects)等等。
JavaScript——包括一系列jQuery的插件,这些插件可以实现组件的动态页面效果。插件主要包括模态窗口(modals),提示效果(tool tips),“泡芙”效果(popovers),滚动监控(scrollspy),旋转木马(carousel),输入提示(typeahead),等等。
Bootstrap已经足够强大,能够实现各种形式的 Web 界面。为了更加方便地利用Bootstrap进行开发,很多工具和资源可以用来配合使用,下面列举了其中的一部分工具和资源。
—— 对于jQuery和Bootstrap爱好者来说这是个非常好的资源,能够把 Bootstrap的清爽界面组件引入到jQuery UI中。
—— 和上面提到的jQuery UI主题类似,这是一个为jQuery mobile建立的主题。如果你想让用Bootstrap开发的网站在手机端也可以优雅访问,那么这个资源对你来说很方便易用。
—— 它为Bootstrap添加了一些轻量的JavaScript控件。Fuel UI 安装,修改,更新以及优化都很简单方便。
—— Bootstrap提供了自己的几种界面风格,StyleBootstrap提供了更多的配色选项,并且你可以给每个组件都应用不同的配色。
—— 利用这个工具你可以立刻查看主题修改后的效果。对于每一次变动的效果,这个应用都会生成一个唯一的URL方便你与他人分享,你也可以在任意时刻修改你的主题。
—— 提供大量免费的Bootstrap主题。
—— 在线前端框架交互组件制作工具,是一个供给设计师和开发者的基于Bootstrap HTML/CSS/JavaScript 架构的免费元素。
—— 通过界面拖放生成器简便快捷地创建基于Bootstrap的前端代码。通过拖放动作将Bootstrap风格的组件加入到你的个人设计里并且可以方便地修改他们的属性,简单便捷。
2. Fbootstrapp
基于Bootstrap并且提供了跟Facebook iframe apps和设计相同的功能。包含用于所有标准组件的基本的CSS和HTML,包括排版、表单、按钮、表格、栅格、导航等等,风格与Facebook类似。
3. BootMetro
框架的灵感来自于Metro UI CSS,基于Bootstrap 框架构建,用于创建Windows 8 的Metro风格的网站。它包括所有Bootstrap的功能,并添加了几个额外的功能,比如页面平铺,应用程序栏等等。
4. Kickstrap
是Bootstrap的一个变体。它基于Bootstrap,并在它的基础上添加了许多app,主题以及附加功能。这使得这个框架可以单独地用于构建网站,而不需要额外安装什么。你需要做的仅仅是把它放到你的网站上,然后用就可以了。
App 是一些页面加载完成之后加载运行的JavaScript和CSS打包文件。默认加载的app有Knockout.js, Retina.js, Firebug Lite, and Updater,你也可以自行添加更多的app。
选择不同的主题可以让你的网站在众多Bootstrap构建的类似网站中显得与众不同。
附加功能是一些用来扩展Bootstrap UI 库的附件,它们的语法基本相同或者相似。
下半部分的内容…
这一部分我们主要总结了Bootstrap相关的一些框架。在下半部分,我们会介绍更多其他的框架。
关于作者:
可能感兴趣的话题
目前正在学习Bootstrap。。。
关于伯乐在线博客
在这个信息爆炸的时代,人们已然被大量、快速并且简短的信息所包围。然而,我们相信:过多“快餐”式的阅读只会令人“虚胖”,缺乏实质的内涵。伯乐在线内容团队正试图以我们微薄的力量,把优秀的原创文章和译文分享给读者,为“快餐”添加一些“营养”元素。
新浪微博:
推荐微信号
(加好友请注明来意)
– 好的话题、有启发的回复、值得信赖的圈子
– 分享和发现有价值的内容与观点
– 为IT单身男女服务的征婚传播平台
– 优秀的工具资源导航
– 翻译传播优秀的外文文章
– 国内外的精选文章
– UI,网页,交互和用户体验
– 专注iOS技术分享
– 专注Android技术分享
– JavaScript, HTML5, CSS
– 专注Java技术分享
– 专注Python技术分享
& 2017 伯乐在线C/C++:构建你自己的插件框架
插件通常以动态库的形式部署。动态库允许插件的很多优势如热交换(重新加载一个插件的新实现而无需关闭系统),而且需要更少的链接时间。然而,在某些情况下静态库是插件的最好选择。例如,仅仅因为某些系统不支持动态库(很多嵌入式系统)。在其他的情况下,出于安全考虑,不允许加载陌生的代码。有时,核心系统会与一些预先加载好插件一起部署,而且静态加载到主系统中使得它们更健壮(因此用户不会无意中删除它们)。
底线是,好的插件系统应当同时支持静态和动态插件。这可以让你在不同的环境下,不同的约束下部署同一个基于插件的系统。
插件编程接口
所以关于插件的问题都是关于接口(译注:要注意这里说的接口,不是C#和JAVA的接口概念,理解为signature更合适)的。基于插件的系统的基本观念是:有某个中央系统,通过定义良好的接口和协议,其在加载插件时不知道任何关于与插件通信的问题。
定义一系列函数作为插件导出的接口(动态库及静态库)是幼稚的方法。这种方法在技术上是可行的,但在概念上是有瑕疵的(译注:作者说话分量还是轻些)。原因是,插件应当支持两种接口且只能有一套从插件导出的函数。这表明这两种接口会被混合在一起。
第一层接口(及协议)是通用的插件接口。它使得中央系统可以初始化插件,并使插件可以在中央系统中注册一系列的用于创建和销毁对象以及全局的清理函数。通用插件接口不是与领域相关的,且可以被指定和实现为可服用的库。第二层接口是由插件对象实现的功能性的接口。该接口是领域相关的,且世纪的插件必须非常谨慎的对其进行设计和实现。中央系统应当知道该接口并能通过其与插件对象进行交互。
列表1是一个指定了通用插件接口的头文件。没有深入细节并解释所有事情之前,让我们看看它提供了什么。
#ifndef PF_PLUGIN_H
#define PF_PLUGIN_H
#include &apr-1/apr_general.h&
#ifdef __cplusplus
extern "C" {
typedef enum PF_ProgrammingLanguage
& PF_ProgrammingLanguage_C,
& PF_ProgrammingLanguage_CPP
} PF_ProgrammingL
struct PF_PlatformServices_;
typedef struct PF_ObjectParams
& const apr_byte_t * objectT
& const struct PF_PlatformServices_ *
} PF_ObjectP
typedef struct PF_PluginAPI_Version
& apr_int32_
& apr_int32_
} PF_PluginAPI_V
typedef void * (*PF_CreateFunc)(PF_ObjectParams *);
typedef apr_int32_t (*PF_DestroyFunc)(void *);
typedef struct PF_RegisterParams
& PF_PluginAPI_V
& PF_CreateFunc createF
& PF_DestroyFunc destroyF
& PF_ProgrammingLanguage
programmingL
} PF_RegisterP
typedef apr_int32_t (*PF_RegisterFunc)(const apr_byte_t *
nodeType, const PF_RegisterParams * params);
typedef apr_int32_t (*PF_InvokeServiceFunc)(const apr_byte_t *
serviceName, void * serviceParams);
typedef struct PF_PlatformServices
& PF_PluginAPI_V
& PF_RegisterFunc registerO
& PF_InvokeServiceFunc invokeS
} PF_PlatformS
typedef apr_int32_t (*PF_ExitFunc)();
typedef PF_ExitFunc (*PF_InitFunc)(const PF_PlatformServices
#ifndef PLUGIN_API
& #ifdef WIN32
&&& #define
PLUGIN_API __declspec(dllimport)
&&& #define
PLUGIN_API
#ifdef& __cplusplus
PLUGIN_API PF_ExitFunc PF_initPlugin(const PF_PlatformServices *
#ifdef& __cplusplus
首先你应当注意到这是一个C文件。这允许插件框架可以由纯C系统编译使用并可用来写纯C插件。但是,它不仅仅局限在C上,且实际上大多数情况下用在C++中。
枚举类型PF_ProgrammingLanguage允许插件声明到用C++实现的插件管理器中。
PF_ObjectParams是一个抽象的结构体,创建插件时用于传递参数给插件对象。
PF_PluginAPI_Version被用于商讨版本问题,并保证插件管理器只加载合适版本的插件。
函数指针PF_CreateFunc和PF_DestroyFunc(由插件来实现)允许插件管理器来创建和销毁插件对象(每个插件注册这样的函数到插件管理器中。)
PF_RegisterParams结构体包含初始化时插件必须提供给插件管理器的所有信息。(版本信息,创建/销毁函数,编程语言)
PF_RegisterFunc(由插件管理器实现)允许每个插件为每种它所支持的对象类型注册一个PF_RegisterParams结构体。注意这个方案允许一个插件注册一个对象的不同版本和多个对象类型。
PF_InvokeService函数指针是一个通用的函数,查检可以用其来调用主系统提供的服务如日志、事件通知及错误报告。其签名(signature)包括服务名称和指向一个参数结构体不透明的指针。插件应当知道可用的服务以及如何调用它们(或者,如果你希望使用PF_InvokeService,你可以自己实现服务。)
PF_PlatformServices结构体聚集了我刚刚提到所有的由平台提供给插件的服务(版本,注册对象,执行服务函数)。该结构体在初始化时传递给每个插件。
PF_ExitFunc定义了插件退出函数,由插件来实现。
PF_InitFunc定义了插件初始化函数指针。
PF_initPlugin是动态插件(由动态链接库/共享库来部署的插件)实际的初始化函数的签名(signature)。从动态插件中导出它的名字,因此插件管理器可以在加载插件时调用它。它接受一个指向PF_PlatformServices结构体的指针,因此所有的服务在初始化时立刻可用(这是注册对象的正确时机)并返回一个指向退出函数的指针。
注意静态插件(实现在静态库中且直接连接到主执行体中)应当实现一个有C
linkage的init函数,但禁止将其命名为PF_initPlugin。原因是如果有多个静态插件,他们都将有一个同样的初始化函数名字,你的编译器痛恨这个。
静态插件的初始化有所不同。他们必须显式地由主执行体初始化。主执行体将通过PF_InitFunc的签名调用它们的初始化函数。很不幸,这意味着每当一个新的静态插件加入/移出系统时,主执行体需要被修改,并且各种各样的init函数的名字必须是对等的(coordinated)。
有一种试图解决该问题的技术叫做“自动注册”。自动注册通过静态库中一个全局的对象来达到目的。该对象在main()事件启动之前被构建。该全局对象可以请求插件管理器来初始化静态插件(通过传递插件的init()函数指针)。不幸的是,这种方案在VC++中不能工作。
撰写插件意味着什么?插件框架是非常generic,并且不提供任何可以与你的应用交互的切实的对象。你必须在插件框架上构建你自己的应用程序模型。这意味着你的应用程序(加载插件)以及插件自身必须同意并通过某种模型来协作。通常这表明应用程序期待插件提供暴露某种特定API的某种类型的对象。插件框架将提供注册、枚举及加载这些对象的基础设施。示例1是一个叫做IActor的C++接口的定义。它有两个操作——getInitialInfo()和play()。注意该接口不是充分的,因为getInitialInfo()期望一个指向名为ActorInfo的结构体的指针,且play()期望一个指向另一个叫做ITurn接口的指针。这是实际的一个案例,你必须设计并指定整个对象模型。
struct IActor
& virtual ~IActor() {}
& virtual void getInitialInfo(ActorInfo * info)
& virtual void play( ITurn * turnInfo) = 0;
每个插件可以注册多个实现了IActor接口的类型。当应用程序决定示例化一个由插件注册的对象,它将调用注册的,由插件实现的PF_CreateFunc函数。插件负责创建一个合适的对象并将其返回给应用程序。返回类型指定为void
*是因为对象创建操作是通用插件框架的一部分,该部分不知道任何关于特定IActor接口的信息。应用程序随后将void
*转换到IActor
*,然后就可以在整个接口中使用它,好像它是一个正常的对象。当应用程序使用完IActor对象后,它执行注册的由插件实现的PF_DestroyFunc函数,然后插件销毁actor对象。目前不好考虑虚拟的析构函数,我会在后面的部分讨论它。
编程语言支持
在二进制兼容性部分我解释了你可以利用C++的vtable一级的兼容性,如果你的编译器满足的话。你也可以使用C一级的兼容性,这样你就可以使用不同的编译器来构建应用程序和插件,但你将被局限在C的交互上。你的应用程序对象模型必须是基于C的。你不能使用好的C++接口如IACTOR,但你必须设计一个相似的C接口。
在纯C的编程模型中你只需要用C开发插件。当你实现PF_CreateFunc函数时你返回一个在你的应用程序C对象模型中与其它C对象交互的C对象。所有的话题都是关于C对象和C对象模型的。所有人都知道C是一个过程语言,没有对象的概念。然而C提供了足够的抽象机制来实现对象以及多态(在此处是必须的)并支持面向对象的编程泛型。实际上,最初的C++编译器是一个C编译器的事实上的一个前端(front-end)。它根据C++代码产生C代码,然后使用一个普通的C编译器来编译该C代码。它的名字Cfront说明了一切。
使用包含函数指针的结构体(译注:就可以获得OO特性)。每个函数的签名应当接受它所属结构体作为第一个参数该结构体也可以包含其它的数据成员。这样提供了(与C++类有关的简单的土语)如:封装(状态和行为捆绑)、继承(通过将基结构体的对象作为第一个数据成员)以及多态(通过设置不同的函数指针。)(译注:没错,这就是用C来编写OO程序的基本要求和方法,我也用C写过OO程序)。
C不支持析构函数、函数及操作符重载,名字空间,因此你定义接口时只有很少的选项。这也许是“塞翁失马,焉知非福”,因为接口应该被可能掌握C++另一个子集的其它人所使用。减少语言的范围可能会提升你的接口的简单性和可用性。
我将在插件框架的后续文章中探究OO的C。列表2包含了陪伴该文章系列(仅仅是投你所好)的示例游戏的C对象模型。如果你快速浏览一下你会看见它甚至支持集合以及遍历。
#ifndef C_OBJECT_MODEL
#define C_OBJECT_MODEL
#include &apr-1/apr.h&
#define MAX_STR 64
typedef struct C_ActorInfo_
& apr_uint32_
apr_byte_t&& name[MAX_STR];
& apr_uint32_t location_x;
& apr_uint32_t location_y;
& apr_uint32_
& apr_uint32_
& apr_uint32_
& apr_uint32_
& apr_uint32_
} C_ActorI
typedef struct C_ActorInfoIteratorHandle_ { } *
C_ActorInfoIteratorH
typedef struct C_ActorInfoIterator_
& void (*reset)(C_ActorInfoIteratorHandle
& C_ActorInfo *
(*next)(C_ActorInfoIteratorHandle handle);
& C_ActorInfoIteratorH
} C_ActorInfoI
typedef struct C_TurnHandle_ { } * C_TurnH
typedef struct C_Turn_
& C_ActorInfo * (*getSelfInfo)(C_TurnHandle
& C_ActorInfoIterator *
(*getFriends)(C_TurnHandle handle);
& C_ActorInfoIterator * (*getFoes)(C_TurnHandle
& void (*move)(C_TurnHandle handle,
apr_uint32_t x, apr_uint32_t y);
& void (*attack)(C_TurnHandle handle,
apr_uint32_t id);
typedef struct C_ActorHandle_ { } * C_ActorH
typedef struct C_Actor_
& void (*getInitialInfo)(C_ActorHandle handle,
C_ActorInfo * info);
& void (*play)(C_ActorHandle handle, C_Turn *
& C_ActorH
在纯C++编程模型中你仅仅需要用C++开发你的插件。插件编程接口函数可以被实现为静态成员函数或者普通的静态/全局函数(毕竟C++主要是C的超集)。(这句不好翻啊:The
object model can be your garden variety C++ object
model.)列表3包含示例游戏的C++对象模型。它基本上与列表2种的C对象模型相似。
#ifndef OBJECT_MODEL
#define OBJECT_MODEL
#include "c_object_model.h"
typedef C_ActorInfo ActorI
struct IActorInfoIterator
& virtual void reset() = 0;
& virtual ActorInfo * next() = 0;
struct ITurn
& virtual ActorInfo * getSelfInfo() = 0;
& virtual IActorInfoIterator * getFriends() =
& virtual IActorInfoIterator * getFoes() =
& virtual void move(apr_uint32_t x,
apr_uint32_t y) = 0;
& virtual void attack(apr_uint32_t id) = 0;
struct IActor
& virtual ~IActor() {}
& virtual void getInitialInfo(ActorInfo * info)
& virtual void play( ITurn * turnInfo) = 0;
C/C++二重奏
在这个编程模型中你可以使用C或者C++来开发插件。当你注册你的对象时要指定编程语言。如果你创建一个平台并且你想提供给第三方开发者最终的自由是他们可以选择自己的编程语言及模型,混合并匹配C和C++插件时,这个模型将非常有用。
插件框架支持它,但实际的工作在于为你的应用设计一个既支持C又支持C++对象模型。每个对象类型需要同时实现C和C++的接口。这意味着你将有一个有着标准VTABLE布局的C++类以及一系列与虚拟方法相关的函数指针。这种结构非常复杂,我将不演示它。
要注意的是从插件开发人员的角度来说这种方法不会带来额外的复杂性。他们永远可以使用C或C++接口来开发C或C++的插件。
C/C++混合体
在该模型中,你必须在C对象模型的盖子之下用C++开发插件。这就包括了C++包裹类的创建,该包裹类实现了C++对象模型并包裹(wrap)相应C对象的。插件开发人员在这层上编程,将每个调用、参数及返回值在C和C++之间翻译。当实现你的应用程序对象模型时这需要额外的工作,但通常很直接。好处是对于插件开发这来说提供了一个有着完整C级兼容性的好的C++编程模型。我不会在示例游戏的上下文中演示它。
语言-链接矩阵
图1显示了各种不同的部署模型组合的利与弊(静态库vs. 动态库)以及编程语言的选择(C vs. C++)。
为了本次讨论,如果使用C++插件,C/C++二重奏模型有C++限制和所需的先决条件,对于C插件,有C的限制和所需的先决条件。而且,C/C++混合模型不过是C模型,因为C++层被隐藏在插件实现后面。这些都让人迷惑,但底线是你有选项了,且插件框架允许你做出自己的决定,采用你自己认为合适的折中。它没有强迫你使用某个特定的模型,也没有瞄准最小公分母。
Copyleft (C)
本文可以用于除商业用途外的所有用途。若用于非商业用途,请保留此权利声明,并以超链接形式标明文章原始出版、作者信息和本声明;若要用于商业用途,请与作者联系,否则作者将使用法律来保证权利。
本文是关于开发跨平台C++插件系列的第二篇。第一篇详细描述了问题,探索了一些解决方案,并介绍了插件框架。本部分描述了架构以及构件在插件框架上,基于插件的系统的设计,插件的生命期,以及通用插件框架的内部。小心:代码遍布文章各个部分。
基于插件系统的架构&
基于插件的系统可以分厂三个松散耦合的部分:有自己特有对象模型的主系统或应用;插件管理器;以及插件本身。插件遵从插件管理器的接口和协议,并实现对象模型接口。让我们用一个实际的例子来展示。主系统是一个基于回合的游戏。游戏发生在一个有着各种各样怪兽的战场上。英雄与怪兽搏斗知道他或者所有的怪兽死掉。列表以是英雄类的定义:
#ifndef HERO_H
#define HERO_H
#include &vector&
#include &map&
&boost/shared_ptr.hpp&
#include "object_model/object_model.h"
class Hero : public IActor
& ~Hero();
& // IActor methods
& virtual void getInitialInfo(ActorInfo *
& virtual void play(ITurn * turnInfo);
Listing one
BattleManager是驱动该游戏的引擎。它负责初始化hero和monster并且将他们安置在战场上。然后每个回合中,它通过调用每个actor(Hero或者Monster)的play()方法来攻击。
Hero和monster实现了IActor接口。Hero是一个内建的,有着预订义行为的游戏对象。另一方面,monster是由插件来实现的。这就允许游戏能够扩展为更多的新monster,并将新的monster的开发和游戏引擎的开发分离开来。PluginManager的工作就是抽象掉monster是由插件来产生并将他们展现给BattleManager
的事实,就像hero。这个方案允许一些内建的monster随同游戏一起发布,这些monster被静态链接进去,不是用插件实现的。BattleManager
甚至不应该知道有插件这样一回事。它应当在C++对象一级进行操作。这也使得其非常易于测试,因为你可以在测试代码中创建一些假的monster而不用写一个完整的插件。
PluginManager本身可以是通用的也可以是特定的。通用的插件管理器不知道特定的底层对象模型。当一个C++的PluginManager实例化一个在插件中实现的新对象,它必须返回一个通用的接口,这样调用方必须将该实例转换成实际的接口。这看起来是有点丑,但很有必要。一个定制的插件管理器知道你的对象模型并且能够从底层的对象模型方面来操作。例如,一个为我们的游戏定制的PluginManager可以有返回IActor
接口的CreateMonster()方法。我所展示的PluginManager是通用的,但我将展示将对象模型特定的一层放到它上面会有多么简单。这是标准的实践,因为你不希望你的应用程序代码来处理显式类型转换。
插件系统生命期
现在该弄明白插件系统的生命期了。应用程序,PluginManager以及插件根据一个严格的协议参与到这个复杂的活动中。好的消息是,通用的插件框架大部分都能很好的安置这些东西。当需要的时候,应用获取插件的访问权,插件仅仅需要实现一些到时候会被调用的函数。
静态插件的注册
由静态库部署并静态链接到应用程序中的插件就是静态插件。其注册可以自动的完成,前提是库定义了一个全局的注册者对象并且该对象的构造函数被自动调用。然而它不能在所有的平台上工作,如M$的Windows。可选的方法是通过传递一个专门的初始化函数来显式告诉PluginManager来初始化静态插件。因为所有的静态插件都静态地链接到了主程序,因此每个init()都应当有一个惟一的名字且必须是PF_InitPlugin类型的。一个好的惯例是将其命名为:&Plugin
Name&_InitPlugin()。下面是静态插件的init()函数的原型:
extern "C" PF_ExitFunc
StaticPlugin_InitPlugin(const PF_PlatformServices * params)
显式初始化在主程序和静态插件之间创建了一个紧密耦合的关系,因为主程序需要在编译期知道什么插件被链接近来以便初始化他们。如果所有的静态插件遵从某种约定从而使得构建(build)过程能够找到他们并产生相应的初始化代码,那么这个过程可以作为构建(build)过程的一部分自动执行。
一旦初始化完成,静态插件将会注册其所有的对象类型到PluginManager中。
动态插件的加载
动态插件更普遍。他们应当全部由一个专门的目录来部署。应用程序应该调用PluginManager的loadAll()方法并传入该目录的路径。PluginManager扫描该目录下所有的文件,并加载每个动态库。应用程序也可以调用load()方法来加载单独的插件。
插件初始化
一旦动态库被成功加载,PluginManager就会查找被称为RPF_initPluginS的函数入口点。如果找到该入口点,PluginManager
通过调用该函数并传递一个F_PlatformServices结构体来初始化它。该结构体包含PF_PluginAPI_Version以便插件与主程序进行版本上的协商并确定是否能够正常工作。若应用程序的版本不合适,插件可能会使初始化失败。PluginManager志记这个问题,然后继续加载下一个插件。从PluginManager角度来说加载或初始化某个插件失败不是一个严重的错误。应用程序可以执行一些额外的检查来保证枚举已加载的插件,这样就能检查是否有重要的插件没有被加载。
Listing Two包含了PF_initPlugin函数:
#include "cpp_plugin.h"
#include "plugin_framework/plugin.h"
#include "KillerBunny.h"
#include "StationarySatan.h"
extern "C" PLUGIN_API apr_int32_t ExitFunc()
& return 0;
extern "C" PLUGIN_API PF_ExitFunc PF_initPlugin(const
PF_PlatformServices * params)
& int res = 0;
& PF_RegisterP
& rp.version.major = 1;
& rp.version.minor = 0;
& rp.programmingLanguage =
PF_ProgrammingLanguage_CPP;
& // Regiater KillerBunny
&&rp.createFunc =
KillerBunny::
& rp.destroyFunc = KillerBunny::
params-&registerObject((const apr_byte_t
*)"KillerBunny", &rp);
& if (res & 0)
&&& return
& // Regiater StationarySatan
& rp.createFunc = StationarySatan::
& rp.destroyFunc =
StationarySatan::
params-&registerObject((const apr_byte_t
*)"StationarySatan", &rp);
& if (res & 0)
&&& return
&& return ExitF
Listing Two
如果版本协商成功进行了,插件就应当把其支持的所有对象类型注册到插件管理器中去。注册的目的是为了提供给应用程序诸如PF_CreateFunc和PF_DestroyFunc的函数以便后续的使用。这个安排允许插件控制对象实际的创建和销毁,包括他们所使用的资源如内存,但让应用程序来控制对象的数量和它们的生命期。当然也可以使用singleton模式来返回同一个对象的实例。
通过为每个对象类型准备注册记录(PF_RegisterParams)并调用PF_PlatformServices
(作为参数传递给PF_initPlugin)结构体里registerObject()函数指针来完成注册。registerObject()
接受一个能惟一标识对象类型的字符串或者“*”以及PF_RegisterParams
struct。我将在下一节解释类型字符串的目的以及如何使用。需要类型字符串的原因是不同的插件可能会支持多种不同的对象类型。
一旦插件调用registerObject()控制又回到了PluginManager。PF_RegisterParams包含了一个版本以及编程语言。版本使PluginManager
能够保证它可以与该对象类型协同工作。版本不符将导致无法注册。这不是个严重错误,这样可以允许相当有弹性的协商。插件试图注册同一类型的多个版本以便能够利用新的接口,且当新接口失败时回到旧接口。如果插件管理器对PF_RegisterParams满意,它将该结构体存放到能够映射对象类型到该结构体的内部数据结构中。
当插件注册完其所有的对象类型,它返回一个指向PF_ExitFunc的指针。该函数在插件被卸载前调用使得插件可以清除在其生命期内获得的所有资源。
若插件发现其不能正常工作,它将清除所有的资源并返回NULL。这样PluginManager
就知道插件初始化失败,并且会删除失败的插件所作的所有注册
作者 weiqubo
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

我要回帖

更多关于 前端构建框架 的文章

 

随机推荐