思考并回答以下问题:
最基本的数据访问程序
原来的数据访问的做法,用‘新增用户’和‘得到用户’为例。
用户类,假设只有ID和Name两个字段,其余省略。
1 | class User |
SqlserverUser类——用于操作User表,假设只有“新增用户”和“得到用户”方法,其余方法以及具体的SQL语句省略。
1 | class SqlserverUser |
客户端代码
1 | class Program |
这里之所以不能换数据库,原因就在于SqlserverUser su = new SqlserverUser()使得su这个对象被框死在SQL Server上了。如果这里是灵活的,专业点的说法,是多态的,那么在执行‘su.Insert(user);’和‘su.GetUser(1);’时就不用考虑是在用SQL Server还是在用Access。
下面用‘工厂方法模式’来封装new SqlserverUser()所造成的变化。
工厂方法模式是定义一个用于创建对象的接口,让子类决定实例化哪一个类。
用了工厂方法模式的数据访问程序
代码结构图
IUser接口,用于客户端访问,解除与具体数据库访问的耦合。
1 | interface IUser |
SqlserverUser类,用于访问SQL Server的User。
1 | class SqlserverUser : IUser |
AccessUser类,用于访问Access的User。
1 | class AccessUser : IUser |
IFactory接口,定义一个创建访问User表对象的抽象的工厂接口。
1 | interface IFactory |
SqlServerFactory类,实现IFactory接口,实例化SqlserverUser。
1 | class SqlServerFactory : IFactory |
AccessFactory类,实现IFactory接口,实例化AccessUser。
1 | class AccessFactory : IFactory |
客户端代码
1 | class Program |
现在如果要换数据库,只需要把new SqlServerFactory()改成new AccessFactory(),此时由于多态的关系,使得声明IUser接口的对象iu事先根本不知道是在访问哪个数据库,却可以在运行时很好地完成工作,这就是所谓的业务逻辑与数据访问的解耦。
但是这样写,代码里还是有指明‘new SqlServerFactory()’,要改的地方,依然很多。
问题没有完全解决,数据库里不可能只有一个User表,很可能有其他表,比如增加部门表(Department表),此时如何办呢?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Department
{
private int _id;
public int ID
{
get { return _id; }
set { _id = value; }
}
private string _deptName;
public string DeptName
{
get { return _deptName; }
set { _deptName = value; }
}
}
那要增加好多类了。
用了抽象工厂模式的数据访问程序
增加关于部门表的处理。
代码结构图
IDepartment接口,用于客户端访问,解除与具体数据库访问的耦合。
1 | interface IDepartment |
SqlserverDepartment类,用于访问SQL Server的Department。
1 | class SqlserverDepartment : IDepartment |
AccessDepartment类,用于访问Access的Department。
1 | class AccessDepartment : IDepartment |
IFactory接口,定义一个创建访问Department表对象的抽象的工厂接口。
1 | interface IFactory |
SqlServerFactory类,实现IFactory接口,实例化SqlserverUser和SqlserverDepartment。
1 | class SqlServerFactory : IFactory |
AccessFactory类,实现IFactory接口,实例化AccessUser和AccessDepartment。
1 | class AccessFactory : IFactory |
客户端代码
1 | class Program |
结果显示如下:1
2
3
4在Access中给User表增加一条记录
在Access中根据ID得到User表一条记录
在Access中给Department表增加一条记录
在Access中根据ID得到Department表一条记录
这样就可以做到,只需更改IFactory factory = new AccessFactory()为IFactory factory = new SqlServerFactory(),就实现了数据库访问的切换了。
只有一个User类和User操作类的时候,是只需要工厂方法模式的,但现在显然数据库中有很多的表,而SQL Server与Access又是两大不同的分类,所以解决这种涉及到多个产品系列的问题,有一个专门的工厂模式叫抽象工厂模式。
抽象工厂模式
抽象工厂模式(Abstract Factory)结构图
AbstractProductA和AbstractProductB是两个抽象产品,之所以为抽象,是因为它们都有可能有两种不同的实现,就刚才的例子来说就是User和Department,而ProductA1、ProductA2和ProductB1、ProductB2就是对两个抽象产品的具体分类的实现,比如ProductA1可以理解为是SqlserverUser,而ProductB1是SqlserverDepartment。
IFactory是一个抽象工厂接口,它里面应该包含所有的产品创建的抽象方法。而ConcreteFactory1和ConcreteFactory2就是具体的工厂了。就像SqlserverFactory和AccessFactory一样。
通常是在运行时刻再创建一个ConcreteFactory类的实例,这个具体的工厂再创建具有特定实现的产品对象,也就是说,为创建不同的产品对象,客户端应使用不同的具体工厂。
抽象工厂模式的优点与缺点
最大的好处便是易于交换产品系列,由于具体工厂类,例如IFactory factory = new AccessFactory(),在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。设计不能去防止需求的更改,那么理想便是让改动变得最小,现在如果要更改数据库访问,只需要更改具体工厂就可以做到。第二大好处是,它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户代码中。事实上,刚才写的例子,客户端所认识的只有IUser和IDepartment,至于它是用SQL Server来实现还是Access来实现就不知道了。
抽象工厂模式可以很方便地切换两个数据库访问的代码,但是如果需求来自增加功能,比如现在要增加项目表Project,就至少要增加三个类,IProject、SqlserverProject、AccessProject,还需要更改IFactory、SqlserverFactory和AccessFactory才可以完全实现。要改三个类。
客户端程序类显然不会是只有一个,有很多地方都在使用IUser或IDepartment,而这样的设计,其实在每一个类的开始都需要声明IFactory factory = new SqlserverFactory(),如果有100个调用数据库访问的类,是不是就要更改100次IFactory factory = new AccessFactory()这样的代码才行?这不能解决要更改数据库访问时,改动一处就完全更改的要求!”
用简单工厂来改进抽象工厂
去除 IFactory、SqlserverFactory和AccessFactory三个工厂类,取而代之的是DataAccess类,用一个简单工厂模式来实现。
代码结构图
客户端代码
1 | class Program |
这里与其用那么多工厂类,不如直接用一个简单工厂来实现,抛弃了IFactory、SqlserverFactory和AccessFactory三个工厂类,取而代之的是DataAccess类,由于事先设置了db的值(Sqlserver或Access),所以简单工厂的方法都不需要输入参数,这样在客户端就只需要DataAccess.CreateUser()和DataAccess.CreateDepartment()来生成具体的数据库访问类实例,客户端没有出现任何一个SQL Server或Access的字样,达到了解耦的目的。
客户端已经不再受改动数据库访问的影响了。如果需要增加Oracle数据库访问,本来抽象工厂只增加一个OracleFactory工厂类就可以了,现在就比较麻烦了。
这样就需要在DataAccess类中每个方法的swicth中加case了。
用反射+抽象工厂的数据访问程序
要考虑的就是可不可以不在程序里写明‘如果是Sqlserver就去实例化SQL Server数据库相关类,如果是Access就去实例化Access相关类’这样的语句,而是根据字符串db的值去某个地方找应该要实例化的类是哪一个。这样,switch就可以对它说再见了。
去某个地方找应该要实例化的类是哪一个’?
依赖注入(Dependency Injection)。关键在于如何去用这种方法来解决我们的switch问题。本来依赖注入是需要专门的IoC容器提供,比如Spring.NET,显然当前这个程序不需要这么麻烦,你只需要再了解一个简单的.NET技术‘反射’就可以了。
只要在程序顶端写上using System.Reflection;来引用Reflection,就可以使用反射来帮我们克服抽象工厂模式的先天不足了。
有了反射,获得实例可以用下面两种写法。1
2//常规的写法
IUser result = new SqlserverUser();
实例化的效果是一样的,但这两种方法的区别在哪里?
常规方法是写明了要实例化SqlserverUser对象。反射的写法,其实也是指明了要实例化SqlserverUser对象。
常规方法不可以灵活更换为AccessUser,都是事先编译好的代码。
在反射中‘CreateInstance(”抽象工厂模式. SqlserverUser”)’,可以灵活更换‘SqlserverUser’为‘AccessUser’”
因为这里是字符串,可以用变量来处理,也就可以根据需要更换。
原来的实例化是写死在程序里的,而现在用了反射就可以利用字符串来实例化对象,而变量是可以更换的。
准确地说,是将程序由编译时转为运行时。由于‘CreateInstance(”抽象工厂模式. SqlserverUser”)’中的字符串是可以写成变量的,而变量的值到底是SQL Server,还是Access,完全可以由事先的那个db变量来决定。所以就去除了switch判断的麻烦。”
代码结构图
DataAccess类,用反射技术,取代IFactory、SqlserverFactory和AccessFactory。
现在如果增加了Oracle数据访问,相关的类的增加是不可避免的,这点无论用任何办法都解决不了,不过这叫扩展,开放-封闭原则性告诉我们,对于扩展,我们开放。但对于修改,应该要尽量关闭,就目前而言,只需要更改private static readonly string db = “Sqlserver”;为private static readonly string db = “Oracle”;也就意味着(IUser)Assembly.Load(AssemblyName).CreateInstance(className);这一句发生了变化。
这样的结果就是DataAccess.CreateUser()本来得到的是SqlserverUser的实例,而现在变成了OracleUser的实例了。
那么如果需要增加Project产品时,如何做呢?
只需要增加三个与Project相关的类,再修改DataAccesss,在其中增加一个public static IProject CreateProject()方法就可以了。
但是,总感觉还是有点缺憾,因为在更换数据库访问时,还是需要去改程序(改db这个字符串的值)重编译,如果可以不改程序,那才是真正地符合开放-封闭原则。
用反射+配置文件实现数据访问程序
还可以利用配置文件来解决更改DataAccess的问题。读文件来给DB字符串赋值,在配置文件中写明是Sqlserver还是Access,这样就连DataAccess类也不用更改了。
添加一个App.config文件。内容如下。1
2
3
4
5
6
<configuration>
<appSettings>
<add key="DB" value="Sqlserver"/>
</appSettings>
</configuration>
再添加引用System.configuration,并在程序开头增加using System.Configuration;,然后更改DataAccess类的字段DB的赋值代码。
1 | class Program |
现在我们应用了反射+抽象工厂模式解决了数据库访问时的可维护、可扩展的问题。从这个角度上说,所有在用简单工厂的地方,都可以考虑用反射技术来去除switch或if,解除分支判断带来的耦合。
switch或者if是程序里的好东西,但在应对变化上,却显得老态龙钟。反射技术的确可以很好地解决它们难以应对变化,难以维护和扩展的诟病。