目 录
第九章 插件引擎设计... 2
9.1 框架的契约-接口... 2
9.2 插件的雏形-抽象类... 3
9.3 实现接口... 4
9.4 反射机制... 5
9.5 反射工具类... 8
9.6 小结... 9
第九章 插件引擎设计
在介绍《第10章 宿主程序详细设计》之前对接口和插件的相关内容进行一下整体介绍,在设计宿主程序的时候会用到这些知识,也是宿主程序与插件之间交互的核心内容。
9.1 框架的契约-接口
插件式框架的宿主程序启动后,它首先会加载相应的配置文件(例如:设备驱动配置文件等),找到相应的插件程序集,这些程序集以DLL文件格式存在,框架的宿主程序会找到指定的插件类型,由插件引擎依据插件类型(例如:IRunDevice)生成对象实例,由框架的宿主程序的管理器对插件实例进行管理和调度。
一个插件程序集可能包括多个插件类型,那么框架宿主程序是如何识别这些类型是否为要加载的插件呢?每个插件对象都有一个身份标识-接口,这个标识在框架设计中被称为“通讯契约”。接口可以被看作是一种定义了必要的方法、属性和事件的集合,因此宿主程序就可以通过这种契约来生成具体的实例对象,并对其他组件或接口公开可操作的对象。
插件式框架作为一个高聚合低耦合的平台,它的功能定义与功能实现之间是分离的。只要符合插件规范的二次开发组件都可以挂载到框架平台中,而它并不并心这些组件的具体功能。当然,框架平台提供了一些必要的信息、机制来保证这些组件能够正常实现二次开发的功能。
在具有多个逻辑层次的结构设计中,各层之间的通信大多通过接口来实现,接口不会轻易改变,如果一个层的功能发生变化,不会影响其他层;只要正常实现了接口的组件功能,那么程序的运行就没有问题。这种做法使得各层之间的相互影响降低到最低,总之,接口在多业务层级中能够更好的解耦。
在大部分功能性的编程和设计工作中,很少需要考虑“接口(interface) ”的情况,如果我们仅仅满足通过控件的方式在IDE上编程和使用.NET Framework中一般的类库,可能永远不会在程序中使用到接口,即使在C#等面向对象语言的语法书中读者会无数次看到过这个词,也只是完成一般性的功能,并未掌握面向对象编程的核心思想。
接口是一般行为的定义和契约。如猫和狗等动物,只需要将一般性的、公共性的属性、动作等定义在接口里,例如:有眼睛、可以吃东西等。尽管不同动物之间存在很大差异,但是接口并不考虑它们各自的特性或功能的差异,例如:什么颜色的眼睛、吃什么东西等。它只关心这些类型都必须实现接口定义的所有功能,而实现了这个接口就可以被看作是一种动物。
因此,接口的两个主要的作用是:
n 定义多个类型都需要的公共方法、属性。
n 作为一种不可实例化的类型存在。
继承接口实现定义的方法、属性等,实际上是实现了一种策略。
9.2 插件的雏形-抽象类
接口与抽象类非常相似,例如两者都不能new一个实例对象,却都可以作
为一种契约和定义被使用。但是接口和抽象类有本质的不同,这些不同包括:
n 接口没有任何实现部分,但是抽象类可以继承接口后部分实现代码。
n 接口没有字段,但是抽象类可以包含字段。
n 接口可以被结构(Struct)继承,但是抽象类不行。
n 抽象类有构造函数和析构函数。
n 接口仅能继承自接口,而抽象类可以继承自其他类和接口。
n 接口支持多继承,抽象类仅支持单根继承。
在MSDN的相关内容中,给出了如下关于接口与抽象类的建议:
n 如果预计要创建组件的多个版本,则创建抽象类。抽象类提供简单易行的方法来控制组件版本。通过更新基类,所有继承类都随更改自动更新。另一方面,接口一旦创建就不能更改,如果要更新接口的版本,必须创建一个全新的接口。
n 如果创建的功能将在大范围的全异对象间使用,则使用接口。抽象类应主要用于关系密切的对象,而接口最适合为不相关的类提供通用的功能。
n 如果要设计小而简练的功能模块,应该使用接口。如果要设计大的功能单元,则应该使用抽象类。
n 如果要在组件的所有实现间提供通用的已实现功能,应该使用抽象类。抽象类允许部分实现类,而接口不包含任何成员的实现。
9.3 实现接口
接口和抽象类都可以作为“通信契约”,为子类提供规范。下面定义一个接口和抽象类。
//定义一个接口 public interface IMyInterface { void Action(int type); string Method(int para); } //定义一个抽象类 public abstract class BaseAbstract:IMyInterface { public abstract void Action(int type); //继承此类抽象类时必须实现这个方法。 public string Method(int para) //实现这个方法 { return para.ToString(); } }
继承接口的话,需要实现全部定义的方法或属性,如下代码:
public class MyClass1:IMyInterface { public void Action(int type) { Console.WriteLine(type.ToString()); } public string Method(int para) { return para.ToString(); } }
继承抽象类的话,只需要实现抽象类没有实现的方法或属性,一般为抽象方法或属性,如下代码:
public class MyClass2:BaseAbstract { public void Action(int type) //继承抽象类,只需要实现这个函数。 { Console.WriteLine(type.ToString()); } }
9.4 反射机制
有了设备驱动或插件,还不能挂载到框架平台的宿主程序中。我们考虑的问题是:已经有了任意多个类型插件程序集,框架平台如何从程序集中根据类型定义在内存中生成插件对象?
回顾普通情况下程序引用其他程序集组件的过程。首先,需要使用“添加引用”对话框加载程序集。然后,通过using关键字引用命名空间。最后,在命令空间下找到相应的类,并new出来一个实例。这是一种静态加载程序集的方式。
在插件式应用框架中,这种方法并不适合。宿主程序在编译时并不知道它将要处理哪些程序集,更没有办法静态的将插件类型通过using关键字引入,这些都是在运行时才能获得的信息。在这样的情况下,也无法使用静态方法和new关键字来生成一个类型实例。而是需要在运行时获得相关信息动态加载程序集,这个过程被称为反射。
反射是动态发现类型信息的一种能力,它类似后期绑定,帮助开发人员在程序运行时利用程序集信息动态使用类型,这些信息在编译时是未知的,反射还支持更高级的行为,如能在运行时动态创建新类型,并调用这些类型的方法等。
JIT编译器在将IL代码编译成本地代码时,会查看IL代码中引用了那些类型。在运行时,JIT编译器利用程序集的TypeRef和AssemblyRef元数据表的记录项来确定哪一个程序集定义了引用的类型。在 AssemblyRef元数据记录项中记录了程序集强名称的各个部分—包括名称,版本,公钥标记和语言文化。这四个部分组成了一个字符串标识。JIT编译 器尝试将与这个标识匹配的程序集加载到当前的AppDomain中。如果程序集是弱命名的,标识中将只包含名称。
.NET Framework中,为了实现动态加载,需要熟悉Assembly、Type和Activator等工具类的方法。框架平台主要使用了Assembly工具类,这个类中包括Load、LoadFrom和LoadFile。
1. Assembly的Load方法
在内部CLR使用Assembly的Load方法来加载这个程序集,这个方法与Win32的LoadLibray等价。在内部,Load导致CLR对程序集应用一个版本重定向策略。并在GAC中查找程序集,如果没有找到,就去应用程序的基目录,私有路径目录和codebase指定的位置查找。如果是一个弱命名程序集,Load不会向程序集应用重定向策略,也不会去GAC中查找程序集。如果找到将返回一个Assembly的引用,如果没有找到则抛出FileNotFoundException异常。注意:Load方法如果已经加载一个相同标识的程序集只会简单的返回这个程序集的引用,而不会去创建一个新的程序集。
大多数动态可扩展应用程序中,Assembly的Load方法是程序集加载到AppDomain的首选方式。这种方式需要指定程序集的标识字符串。对于弱命名程序集只用指定一个名字。
2.Assembly的LoadFrom方法
当我们知道程序集的路径的场合,可以使用LoadFrom方法,它允许传入一个Path字符串,在内部,LoadFrom首先调用AssemblyName的静态方法GetAssemblyName。这个方法打开指定的文件,通过AssemblyRef元数据表提取程序集的标识,然后关闭文件。随后,LoadFrom在内部调用Assembly的Load方法查找程序集。到这里,他的行为和Load方法是一致的。唯一不同的是,如果按Load的方式没有找到程序集,LoadFrom会加载Path路径指定的程序集。另外,Path可以是URL。
3.Assembly的LoadFile方法
这个方法初一看和LoadFrom方法很像。但LoadFile方法不会在内部调用Assembly的Load方法。它只会加载指定Path的程序集,并且这个方法可以从任意路径加载程序集,同一程序集如果在不同的路径下,它允许被多次加载,等于多个同名的程序集加载到了AppDomain中,这一点和上面的两个方法完全不一样。但是,LoadFile并不会加载程序集的依赖项,也就是不会加载程序集引用的其他程序集,这会导致运行时找不到其他参照DLL的异常。要解决这个问题,需要向AppDomain的AssemblyResolve事件登记,在回调方法中显示加载引用的程序集。类似于这样:
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { if (args.Name != null) { return Assembly.LoadFrom(string.Format("{0}\\plugin\\{1}.dll", Application.StartupPath, new AssemblyName(args.Name).Name)); } return null; }
特别注意:要测试LoadFile有没有加载引用的DLL,切不可将DLL拷贝到应用程序的根目录下测试,因为该目录是CLR加载程序集的默认目录,在这个目录中如果存在引用的DLL,它会被加载,造成LoadFile会加载引用DLL的假象。可以在根目录下新建一个子目录如plugin,把引用的dll拷贝到这里面进行测试。
反射机制也有它的缺点:安全性和性能方面。但是,框架平台在启动的时候、以及增加新设备驱动(插件)的时候需要使用反射,一旦加载到宿主程序中,与静态引用程序集没有本质区别,都是寄存在内存中。
9.5 反射工具类
插件式框架平台使用反射挂载设备驱动,在宿主程序中运行,需要一个专用的工具类来完成相关功能。代码定义如下:
/// <summary> /// 一个轻便的 IObjectBuilder 实现 /// </summary> public class TypeCreator : IObjectBuilder { public T BuildUp<T>() where T : new() { return Activator.CreateInstance<T>(); } public T BuildUp<T>(string typeName) { return (T)Activator.CreateInstance(Type.GetType(typeName)); } public T BuildUp<T>(object[] args) { object result = Activator.CreateInstance(typeof(T),args); return (T)result; } /// <summary> /// 框架平台主要使用了这个函数。 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="assemblyname"></param> /// <param name="instancename"></param> /// <returns></returns> public T BuildUp<T>(string assemblyname, string instancename) { if (!System.IO.File.Exists(assemblyname)) { throw new FileNotFoundException(assemblyname + " 不存在"); } System.Reflection.Assembly assmble = System.Reflection.Assembly.LoadFrom (assemblyname); object tmpobj = assmble.CreateInstance(instancename); return (T)tmpobj; } public T BuildUp<T>(string typeName, object[] args) { object result = Activator.CreateInstance(Type.GetType(typeName), args); return (T)result; } }
9.6 小结
下一章节介绍宿主程序详细设计,需要对反射机制有一定的了解,并且会使用到上面的工具类,并在此基础上进行扩展。
框架平台就要完全了,只需要一小步了。
作者:唯笑志在
Email:504547114@qq.com
QQ:504547114
.NET开发技术联盟:54256083
文档下载:http://pan.baidu.com/s/1pJ7lZWf
官方网址:http://www.bmpj.net