原标题:.NET三层架构的改进以及通用数据库访问组件的实现
-
前言
微软的三层架构示例项目PetShop是很多.NET开发人员学习项目开发的经典样本代码,很多人都是直接使用PetShop的架构来搭建项目,
甚至许多著名的代码生成器,也是以PetShop架构作为模板生成项目。事实上我认为PetShop只是一个展示如何使用.NET技术的应用项
目范例,若直接照搬用来用作实际项目开发的框架,还有很多地方需要改进,尤其是在实现多数据库兼容方面。下面以PetShop为样本
代码,阐述如何通过改造它实现一个更合理实用的三层架构。由于改良方法主要采用通用数据库访问组件简化原架构的数据访问层,因此
本文也谈及了如何实现基于ADO.NET的通用数据访问组件。
-
架构分析
-
架构主干
为了便于剖析项目的,下面列举网上一个"PetShop粉丝"的项目架构与PetShop的对照
刨去一些本文讨论无关的一些子项目,可以看出两者的主干项目大致相同。项目架构的各层核心模块调用关系如下图
项目cofcms在数据访问层实现了对SQL Server和DataAccess两种数据库的支持,项目cofcms在数据访问层实现了对SQL Server和
DataAccess两种数据库的支持, 而PetShop则实现了对SqlServer 和 Oracle的支持。
-
代码分析
下面我们以PetShop在UI层Product页面的一行代码作为切入点,分析其架构特点。
很显然,代码的含义是获取用户选择的产品分类作为参数,然后调用product类的GetProductsByCategory方法,取得该分类的所有产
品并作显示。我们进一步查看GetProductByCategory方法的定义,发现位于BLL层。
查看方法体的代码,主要是作了对无效参数的处理,然后调用IDAL层IProduct接口的实现类的GetProductsByCategory方法。
IProduct的实现类实例通过抽象工厂方式创建,由配置文件指定。实现了IDAL层接口的项目有两个,SQLServerDAL和OracleDAL,
在这两个项目下找到实现了IProduct的两个实现类。
通过比较,我们发现这两个实现类的代码几乎一模一样,不过是把SqlParmeter换成OracleParmeter,SqlDbType换成OracleType,
SqlDataReader换成OracleDataReader,SqlHelper换成OracleHelper。我们再比较SqlHelper和OracleHelper里的ExecuteReader方法。
, SqlHelper.cs
OracleHelper.cs
代码结构一模一样,同样的只不过是把用于访问SqlServer的ADO.NET对象,统统换成了相应的访问Oracle的ADO.NET对象。
我们再看看这些不同数据库的ADO.NET类,都继承于System.Data.Common命名空间下的一组标准ADO.NET抽象类。
-
主要改进思路和实现
- 利用ADO.NET抽象基类实现通用数据库访问组件
通过对代码的分析,我们可以看到,如果用抽象基类替换继承子类就可以实现一个通用的数据库访问类DBHelper,还是以ExecuteReader为例,
代码如下
- 使用通用数据库访问组件简化项目架构
我们暂且不讨论DbConnection这个抽象基类如何创建出来(后面 三. 4会提到)。先看看用DBHelper取代了SQLHelper和
OracleHelper后的变化。对于SQLServerDAL 和OracleDAL来说,不同之处主要就是分别使用SQLHelper和OracleHelper
来访问数据库,现在这两个Helper已经统一抽象为DBhelper,那么这几个DAL也顺理成章的统一CommonDAL,无须定义
IDAL接口,针对不同数据库作不同的实现(SQLServerDAL,OracleDAL)了。CommonDAL层GetProductsByCategory方法代码如下:
至于BLL层的代码,简便许多,可以直接创建DAL对象,无须通过抽象工厂方式创建接口IDAL的实现实例了。而用户界面层代码则完全不变。
- 改进后的项目架构
经过重构,我们目前的项目架构各层核心模块调用关系演变如下
形成的项目架构简化如下:
当Web层需要GetProductsByCategory方法时,实现流程演变如下:
- CommonDAL层里的Orders类添加GetProductsByCategory,方法方法体内通过调用 DBHelper,
以及指定的sql语句执行数据库查询返回数据。 - BLL层理的Orders类添加GetOrderById方法,方法体内对id参数进行有效验证后调用CommonDAL
层里的Orders类的GetOrderById方法返回数据。 - Web层直接调用BLL层Orders类的GetOrderById方法
你看,原来需要在IDAL 的IProduct接口添加一个方法定义,SqlServerDAL层和OracleDAL层的Product类都要实现该方法,
然后BLL层,Web才能相继实现调用,现在是不是简单省事多了呢?
-
实现关键点:DbProviderFacoty
前面啰啰嗦嗦讲了这么多,其实就是一句话,用ADO.NET的抽象基类取代原来的子类 ,抽象出通用数据库访问类,
使得开发人员无须再针对n种数据库实现n个DAL。然而,类似原来BLL调用IDAL接口时,需要加载配置文件指定的
某种数据库的实现层DAL模块,通过抽象工厂方式创建接口的实现类一样,现在当BLL调用CommonDAL里的
DBHelper时,里面的ADO.NET抽象基类也需要由针对指定某种数据库的某类ADO.NET组件子类来进行实例化,
实际上,ADO.NET组件里已经定义了这样的一个工厂类,专门负责创建ADO.NET组件。
所以,通过该工厂类来创建ADO.NET组件,我们便已可以把指定ADO.NET组件抽象基类的创建,统一交由该工厂类来负责,
而用哪一类数据库(MSSQL,Oracle,OleDB…)的ADO.NET组件子类来实例化其抽象基类,则决定于该工厂抽象基类用哪一类数据库
的工厂子类来创建,换言之,支持哪种数据库(MESQL,Oracle…)的决定权在于DbProviderFacoty用哪个数据库的继承子类
(SqlClientFacoty,OracleClientFacoty)来实例化,所以,DbProviderFacoty如何创建出来便是全局关键所在。
-
DbProviderFacoty的创建
事实上Visual Stduio里面通过图形化界面建立数据库连接便是使用DbProviderFacoty实现多数据库访问的实例。当我们打开
Visual Sdudio在项目下点击PropertiesàSettings.Settings,打开页面,为当前应用程序配置数据库连接时,Visual Sdudio
会自动在项目配置文件生成数据连接的配置节点。以SQL Server为例,大致如下
<connectionStrings>
<add name="*******.Properties.Settings.MSSQL" connectionString="****** "
providerName="System.Data.SqlClient" />
</connectionStrings>
这里providerName节点有何含义呢?我们打开.NET的系统级配置文件C:\Windows\Microsoft.NET\Framework\v2.0.50727\
CONFIG\machine.config,其中便有关于DbProviderFactory的配置
<system.data>
<DbProviderFactories>
...
<add name="OracleClient Data Provider" invariant="System.Data.OracleClient"
description=".Net Framework Data Provider for Oracle"
type="System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
<add name="SqlClient Data Provider" invariant="System.Data.SqlClient"
description=".Net Framework Data Provider for SqlServer"
type="System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
...
</DbProviderFactories>
</system.data>
很显然,在DbProviderFactories 里是DbProviderFactory的多个代表不同数据库的子类信息集合,拿invariant属性的值与
数据库连接串里providerName匹配,便可得到当前数据库连接所对应的DbProviderFactory子类信息,再通过反射,便可
得到DbProviderFactory的子类实例了。当然,关于machine.config配置信息的读取以及数据源的配置,这里面也有很多内
容,但与本文主题关系不大,不再赘述。有兴趣的读者可以上微软官方网站查阅,我这里只是通过列举微软对DbProvider
Factory的应用,阐述如何创建DbProviderFactory。事实上,我认为也可以自定义一个配置文件存储DbProviderFactories,
在连接串里指定providerName,使用抽象工厂模式实现DbProviderFactory的创建。
- 其他改进
- 当然,要让DAL通过配置而非修改代码的方式适应不同的数据库,还有其它产生差异的地方需要进行处理。比如不同的
数据库的sql语法是有所差异的,PetShop里DAL层定义的sql语句,我们可以把它们放置于外部的XML文件下,通过读
取xml节点获取相应的sql语句。通过配置文件方式为不同的数据库指定使用不同的xml文件,也可以只使用一个xml文件,
切换不同的数据库的时候用同名的xml文件去替换(如果应用系统只使用一种类型的数据库),再如 字段的数据类型也有
所不同,再进行数据库设计时就要充分考虑使用标准化的,可转换的数据类型,如Oracle中的Number类型会被ADO.NET
处理为Decimal,而SQLServer中的int则会转换为int,这样就不妨用Number类型定义整数类型的字段。再有一些更大的差
异,如大字段,二进制数据的处理等,可以考虑使用接口方式,初始化时注入针对当前数据库的实现类。 - DBUtility项目的数据库连接串。DBUtility项目里的DBHelper为DAL层调用,本来可以看作是DAL的低层,作为引用库文
件封装在DAL里,但偏偏这里读取数据库连接是直接从表现层的配置文件里读取,既限造成了连接节点的名称的硬编码,
也无法支持同时对多个数据库的操作。从某种意义上来说造成了对界面层的依赖,使得各层之间的依赖形成了糟糕的闭环。
为了实现各层之间的单向依赖,同时把与业务无关的DBHelper封装成可复用的标准化数据库访问组件,成为DAL的引用dll,
简化框架结构,我认为数据库连接串还是应该由表现层传递至数据访问层,至于传递的方法,可以使用单例模式创建工厂对象,
通过工厂对象创建下层服务对象,而在工厂对象创建时始化数据库连接等一系列相关信息。这样就避免了在调用下层服务
对象时把连接参数传递进去。
-
后记
三层架构是老生常谈了,很多程序员都耳熟能详,但是很多初学者甚至老鸟、大虾,在搭建项目架构时,都是参考PetShop,
依葫芦画瓢,搞个IADL接口层,再整N个DAL作为实现,将所有sql语句,逻辑代码都放入一个DAL,然后再Copy一份到其他
的DAL稍作修改, BLL就仅仅写个转发方法供应用层调用,空荡荡无任何实质内容。名为三层,实则两层,我认为还是对ADO
.NET缺乏深入了解导致的,而现在涌现出各种ORM框架(Entity Framewokr,SQL To Linq,Nhibernate等),以实体对象方式
操作数据,虽然在一定程度上让开发者隔离了底层ADO.NET,但ORM并非万能,我认为目前甚至将来很长一段时间也不能完全
取代ADO.NET,遇到复杂SQL或者对性能要求很高的地方,仍然需要直接调用ADO.NET,而且,这些ORM框架本身就是构建于
ADO.NET之上,所以改进、扩展甚至自主开发ORM框架,都需要我们深入了解并掌握ADO.NET的用法。