创建VS.Net可插拔组件
将VS.Net的反射技术应用于简单工厂模式,建立可插拔的软件组件
文/雷扬
摘要
本文将从设计模式中的简单工厂模式入手,并对简单工厂将会出现的一些扩展性方式进行更为详细的描述,接着将应用VS.Net反射技术对简单工厂提出扩充和优化,从而又介绍了在开发中能够更加方开发过程的可插拔组件的构造。
本文中将以一个典型的三层架构中的数据访问层作为例子,对可插拔组件进行详细的构造。相信读者可以从中获得将可插拔组件应用于其他功能性组件的开发中的启示。
1. 简单工厂
简单工厂,相信每一个熟悉设计模式的软件开发人员都不会陌生,它将使程序员在创建对象的过程中减少对具体类的依赖,扩充面向对象中多态性的意义。
如图表1所示,当我们需要创建一个Product实例的时候就可以通过一个简单工厂ProductFactory中的静态函数GetInstance获得,客户端程序并不需要知道这个对象的具体类型,应用一个抽象的基类或者接口就可以实现各种需要的操作,例如在这个例子中调用Do函数,享受由多态性带来的各种益处。
图表 1
然而,如果我们对简单工厂模式进行具体分析,其中工厂ProductFactory的GetInstance函数必然包括对参数type的解析,形成如代码1所示的代码:
static public Product GetInstance( string type )
{
switch ( type )
{
case "ProductA" : return new ProductA();
case "ProductB" : return new ProductB();
default: throw new Exception("Un-support product type");
}
}
代码 1
某种程度上来说,在这样的结构方式中,我们还是存在客户端程序,ProductFactory和Product三者之间的互相依赖,虽然简单工厂已经对这种依赖进行了简化,但是一些时候还是会引起维护上的麻烦。例如当我们需要增加一个新的Product派生类,例如ProductC,并且需要通过ProductFactory进行实例化这个派生类的情况下,我们需要做的事至少经过以下几个步奏:
1. 打开ProductFactory所在的项目,
2. 找到ProductFactory中的GetInstance函数,
3. 在该函数中增加一个新的case语句case "ProductC" : return new ProductC();
4. 重新编译发布
如此长期以往,随着这个派生类的增加和变化,如此这样的扩展和修改也就将无休地进行。尽管在设计模式中又对简单工厂进行了扩展,形成了工厂方法和抽象工厂等创建形模式[1],但是这些模式都逃不出对一个或大或小的简单工厂的依赖。
2. 反射(Reflection)
反射技术[2]为Microsoft在VS.Net Framework中所提供的一项可以在运行时处理对象和类型的技术,其中涵盖了很多的对类型的操作,这里将主要采用反射技术中获得一个类型的实例的方法。从中我们可以找到一个以不变应万变的简单工厂模式的解决方案,避免对简单的工厂的反复修改。
应用这种反射方法,我们通过获得描述一个类的类型,来获得该类型的构造函数,调用该构造函数就可以获得这个类的一个实例。
在如代码2所示的代码段中,我们用一个类的绝对路径(包括命名空间和类名)和该类型所存在的动态链接库(dll)名称来描述一个类型,如”Demo.Product.ProductA, Product”,这个类型以string的形式存放在type变量中,反射就可以根据这个type信息取得所指定类型的实例。ConstructorInfo是我们通过类型实例获得的类型构造函数,通过调用这个构造函数我们就可以直接获得了所需要的实例。
Type reflectType = Type.GetType(type);
ConstructorInfo constructorInfo = reflectType.GetConstructor(Type.EmptyTypes);
return constructorInfo.Invoke(null) as Product;
代码 2
虽然应用反射获得的实例是一个object类型的实例,但是我们可以将其进行强制转换,并应用于上面简单工厂的例子中,将之前需要不断增改的case语句用反射取而代之,形成如代码3中所示的代码中所实现的新型的简单工厂。
public class ProductFactory
{
static public Product GetInstance( string type )
{
//获得类型
Type reflectType = Type.GetType(type);
//获得类型的构造函数
ConstructorInfo constructorInfo = reflectType.GetConstructor(Type.EmptyTypes);
//利用构造函数创建实例
return constructorInfo.Invoke(null) as Product;
}
}
代码 3
这个新型的简单工厂的最大好处在于不管以后增加多少个Product派生类的定义,我们都能始终保持该工厂使用于运行时的状态。不同的只是客户端在调用该方法的时候需要改变参数type的定义方式,例如代码4中的代码段,固然这里也需要知道一些具体实例化类型的信息,但是由string方式定义类型已经可以在很大程度上使得程序的应用由编译时向运行时转变。
ProductFactory.GetInstance("Products.ProductA, Products").Do();//调用ProductA
ProductFactory.GetInstance("Products.ProductB, Products").Do();//调用ProductB
ProductFactory.GetInstance("Products.ProductC, Products").Do();//调用ProductC
代码 4
应用该套简单工厂和反射的基本原理,我们可以对我们软件中的一些需要应用简单工厂实例化的类进行进一步的优化,创建可插拔的组件体系。
3. 可插拔组件
可插拔组件源于计算机的硬件组合。我们都知道,当我们需要装配一台电脑的时候,我们需要主板,CPU,显示卡,声卡,内存等各种组件。刚开始的时候,这些组件是电脑组装商所提供的一块集成电路板,但是随着需要的增加或者事态的变化,更换组件的需求将会产生,例如升级显示卡。
在我们面对这样的需求,谁都不会希望在更换这一个零部件的时候同时需要更换其它部件,尤其是显示卡的客户端组件,主板。于是聪明的硬件提供商就想出了一个接口的方法,在主板上设立一个不变的插槽(接口),各种品牌的显示卡只要满足这个接口的要求,就可以将它插入插槽和主板一起工作。
软件中的可插拔组件也类似的这样的工作方式。这里就一个大家都可能面对的问题,数据库连接,阐述可插拔组件的创建,相信每一个有经验的读者都可以从中获得构造其它可插拔的功能性组件的启示。
3.1 例子-插拔数据访问层
在数据库项目开发中,我们会根据合理的软件架构方式将应用程序分解为几个层次,形如图表2所示。在VS.Net中这类结构可以方便进行构造,如硬件组成方法一样,数据访问层将履行对数据库访问的职责。
图表 2
如果我们的软件体系需要升级,例如需要从Access数据库升级为SQL Server。和升级硬件时一样,也没有人会希望在更换了数据库类型的时候需要改变表现或者逻辑层。
面向对象的多态性无疑是一个合理的解决方案。如果逻辑层能够通过统一的数据访问层接口或者基类访问数据库,减小它们之间的耦合,当我们切换不同的数据库的时候,需要处理的只是进行一个类似前面描述的简单工厂应用中对抽象基类Product的实例化过程切换。
前面例子中的Product就等同现在所需要的数据访问层抽象基类。逻辑层和简单工厂的调用关系如图表3所示的时序图,逻辑层通过DataProviderFactory获得抽象基类AbstractDataProvider实例,再通过这个实例进行数据库操作,从而减少了对数据访问层的直接依赖。如此这般,对一个应用程序的某一个组成部件的切换就形成了可插拔组件的构建需要。
图表 3
参照前面所描述的Product架构方式,我们的DataProvider可以遵循如图表4所示的继承关系。其中,GetDataBySQL和ExecuteSQL将分别完成对有或者没有返回数据集的SQL语句的执行(在本文中不对这个部分进行详细的描述)。LoadConfig将负责对不同的配置信息进行解析,由于对于不同的数据库会拥有不同的配置信息,例如Access数据库需要指定对应的数据库文件,SQL Server数据库需要指定服务器名称,在这里将AbstractDataProvider中的LoadConfig声明为虚方法,在其派生类中重载这个方法解析不同的用XML表示的配置信息。
图表 4
这样的结构方式加上一个类似前面章节所描述的简单工厂,我们可以简化逻辑层对数据访问层的直接依赖,能够在需要对数据库进行移植的时候保持其他两个层次的不变。其具体代码和结构大家都可以参照前面的简单工厂方式得出。
3.2 IConfigurationSectionHandler
虽然在上面的结构中我们可以通过数据访问层抽象结构,和应用反射的简单工厂获得实例,但是VS.Net中还可以应用更为方便的切换组件的机制,应用配置文件就是其中一种。
IConfigurationSectionHandler为Microsoft VS.Net Framework所提供的控制应用程序配置文件(web.config或者app.config)的诸多接口中的一个。其主要功能可以从一个应用程序的配置文件中获得一个section的XML配置。
在代码4中的web.config配置代码分别用configSection节点中的DataProvider声明和DataProvider节点描述了一个DataProvider配置信息,通过实现接口IConfigurationSectionHandler的可以很方便地对DataProvider节点进行需要的操作[4]。实现我们的集配置与反射为一体的可插拔模型。
<?xmlversion="1.0"encoding="utf-8"?>
<configuration>
<configSections>
<sectionname="DataProvider"type="Demo.DataProvider.DataProviderFactory, Demo.DataProvider"/>
</configSections>
……
<DataProvidertype="Demo.DataProvider.SQLDataProvider, Demo.DataProvider"server="(local) "username="sa"password="sa"database="demo">
</ DataProvider>
</configuration>
代码 4
IConfigurationSectionHandler定义了一个Create"Demo.DataProvider.DataProviderFactory, Demo.DataProvider"形式,当完成了这些配置信息和类的创建后,应用程序就可以通过VS.Net提供的类ConfigurationSetting中的静态函数GetConfig("SectionName")的激活这个实现类的Create,我们将DataProviderFactory,具体的UML静态类图如图表5所示。作为这个实现类方法,并且以参数形式传入以SectionName命名的XmlNode配置信息。在这里方法,形如代码5所示。而且需要在配置文件申明中用type属性的反射格式定义实现该接口的类的全路径,如代码4中所描述的
Object Create(object parent, object configContext, XmlNode section)
代码 5
对于这样的结构,当应用程序调用函数GetConfig(“DataProvider”),将激活我们DataProviderFactory中定义的函数Create。并将配置中的DataProvider节点用参数section传入。
图表 5
这里我们应用的静态函数GetInstance作为调用者,形成如代码6中完整的DataProviderFactory代码。
public class DataProviderFactory:System.Configuration.IConfigurationSectionHandler
{
public DataProviderFactory()
{
}
static public AbstractDataProvider GetInstance(string type )
{
return ConfigurationSettings.GetConfig("DataProvider") as AbstractDataProvider;
}
#region IConfigurationSectionHandler 成员
public object Create(object parent, object configContext, System.Xml.XmlNode section)
{
//通过反射获得实例
Type type = Type.GetType(section.Attributes["type"].InnerText);
ConstructorInfo constructorInfo = type.GetConstructor(Type.EmptyTypes);
AbstractDataProvider result=constructorInfo.Invoke(null) as AbstractDataProvider;
//初始化配置
result.LoadConfig( section);
return result;
}
#endregion
}
代码 6
其中我们应用了存放在DataProvider节点中的type属性(如代码4中表示的Demo.DataProvider.SQLDataProvider, Demo.DataProvider)对所需要的具体类型进行了反射,并调用LoadConfig函数利用面向对象的多态性对不同的AbstractDataProvider派生类对象初始化了不同的基本信息。
以如此的结构方式,我们就完成了一个对可插拔的DataProvider组件的创建,这个可插拔方案解决了对于变化数据库类型时逻辑层对数据访问层和简单工厂的依赖,同时当需要面对切换新的数据访问层类型,就算是目前还未出现的类型的时,我们需要做的也就是实现一个继承于AbstractDataProvider的派生类,再相应地修改相应的配置文件即可完成,完全的保全了工厂,逻辑层,表现层继续运行于运行时的状态。这样的构建方式同样也可以应用于更多的具有插拔需要的功能性组件。
4. 结束语
应用VS.Net提供反射机制实现的简单工厂可以在很大限度上扩充简单模式的扩展性,在更大程度上达到close modification, open extension的目的。配置文件的应用又将在更大程度上简化简单工厂的运作机制,实现可以插拔的软件组件。然而,在软件设计和VS.Net Framework应用的征途中我们还会有更多的领域需要去探索,使得我们的软件产品更加具有可维护性和可扩展性。
参考文献
1. James W.Cooper; C#设计模式;张志华,刘云鹏译;电子工业出版社;2003
2. Reflection Overview;Microsoft MSDNwebsite;visited on 28th June 2006;http://msdn2.microsoft.com/en-us/library/f7ykdhsy.aspx;
3. IConfigurationSectionHandler接口;Microsoft MSDN website;visited on 29th June 2006;http://msdn2.microsoft.com/zh-cn/library/system.configuration.iconfigurationsectionhandler.aspx
4. VS.Net 配置文件概述;http://chs.gotdotnet.com/quickstart/aspplus/doc/configformat.aspx