内容摘录自《大话设计模式》
需求:
已开发好的项目,电子商务网站,是用SQL Server作为数据库的。现在接到另一家公司的类似需求的项目,只能用Access,于是需要改造原来项目的代码。
而实际上,SQL Server和Access的命名空间,使用的函数,关键字等都存在不同之处。
最基本的数据访问程序
用户类,假设只有ID和Name两个字段,其余省略。
//数据表用户类
class User
{
private int _id;
public int ID
{
get { return _id; }
set { _id = value; }
}
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
}
SqlserverUser类——用于操作User表,假设只有“新增用户”和“得到用户”方法,其余省略。
class SqlserverUser
{
public void Insert(User user)
{
Debug.Log("在SQL Server中给User表增加一条记录");
}
public User GetUser(int id)
{
Debug.Log("在SQL Server中根据ID得到User表一条记录");
return null;
}
}
客户端代码
User user = new User();
SqlserverUser su = new SqlserverUser(); // 与SQL Server耦合
su.Insert(user);
su.GetUser(1);
这里不能换数据库,因为SqlserverUser su = new SqlserverUser()使得su这个对象被框死在SQL Server上了。如果这里是灵活的,专业点的说法,是多态的,那么在执行‘su.Insert(user);’和‘su.GetUser(1);’时就不用考虑是在用SQL Server还是在用Access。
下面用“工厂方法模式”来封装new SqlserverUser()所造成的变化。工厂方法模式是定义一个用于创建对象的接口,让子类决定实例化哪一个类。
工厂方法模式的数据访问程序
Iuser接口,用于客户端访问,解除与具体数据库访问的耦合。
// 数据库操作
interface IUser
{
void Insert(User user);
User GetUser(int id);
}
SqlserverUser类,用于访问SQL Server的User。
class SqlserverUser : IUser
{
public void Insert(User user)
{
Debug.Log("在SQL Server中给User表增加一条记录");
}
public User GetUser(int id)
{
Debug.Log("在SQL Server中根据ID得到User表一条记录");
return null;
}
}
AccessUser类,用于访问Access的User。
class AccessUser : IUser
{
public void Insert(User user)
{
Debug.Log("在Access中给User表增加一条记录");
}
public User GetUser(int id)
{
Debug.Log("在Access中根据ID得到User表一条记录");
return null;
}
}
IFactory接口,定义一个创建访问User表对象的抽象工厂接口。
// 工厂类
interface IFactory
{
IUser CreateUser();
}
SqlserverFactory类,实现Ifactory接口,实例化SqlserverUser。
class SqlServerFactory : IFactory
{
public IUser CreateUser()
{
return new SqlserverUser();
}
}
AccessFactory类,实现Ifactory接口,实例化AccessUser。
class AccessFactory : IFactory
{
public IUser CreateUser()
{
return new AccessUser();
}
}
客户端代码
User user = new User();
IFactory factory = new SqlServerFactory(); // 若要更改成Access数据库,只需要将本句改成IFactory factory = new AccessFactory();
IUser iu = factory.CreateUser();
iu.Insert(user);
iu.GetUser(1);
现在如果要换数据库,只需要把new SqlServerFactory()改成new AccessFactory(),此时由于多态的关系,使得声明Iuser接口的对象iu事先根本不知道是在访问那个数据库,却可以在运行时很好的完成工作,这就是所谓的业务逻辑与数据访问的解耦。
仍然存在的问题:代码里还是有指明new SqlServerFactory();如果除了User表外,要增加其他表,需要继续增加类。
抽象工厂模式的数据访问程序
增加部门表(Department)。
代码结构图
部门类,假设只有ID和Name两个字段,其余省略。
class Department
{
private int _id;
public int ID
{
get { return _id; }
set { _id = value; }
}
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
}
IDepartment接口,用于客户端访问,解除与具体数据库访问的耦合。
interface IDepartment
{
void Insert(Department department);
Department GetDepartment(int id);
}
SqlserverDepartment类,用于访问SQL Server的Department。
class SqlserverDepartment : IDepartment
{
public void Insert(Department department)
{
Debug.Log("在SQL Server中给Department表增加一条记录");
}
public Department GetDepartment(int id)
{
Debug.Log("在SQL Server中根据ID得到Department表一条记录");
return null;
}
}
AccessDepartment,用于访问Access的Department。
class AccessDepartment : IDepartment
{
public void Insert(Department department)
{
Debug.Log("在Access中给Department表增加一条记录");
}
public Department GetDepartment(int id)
{
Debug.Log("在Access中根据ID得到Department表一条记录");
return null;
}
}
IFactory接口,定义一个创建访问User表对象的抽象的工厂接口。
// 工厂类
interface IFactory
{
IUser CreateUser();
IDepartment CreateDepartment(); // 增加创建部门表对象的接口方法
}
SqlServerFactory,实现Ifactory接口,实例化SqlserverUser和SqlserverDepartment。
class SqlServerFactory : IFactory
{
public IUser CreateUser()
{
return new SqlserverUser();
}
public IDepartment CreateDepartment() // 增加了SqlserverDepartment工厂
{
return new SqlserverDepartment();
}
}
AccessFactory,实现Ifactory接口,实例化AccessUser和AccessDepartment。
class AccessFactory : IFactory
{
public IUser CreateUser()
{
return new AccessUser();
}
public IDepartment CreateDepartment() // 增加了AccessDepartment工厂
{
return new AccessDepartment();
}
}
客户端代码
User user = new User();
Department department = new Department();
IFactory factory = new SqlServerFactory();
//IFactory factory = new AccessFactory(); // 数据库访问的切换
IUser iu = factory.CreateUser();
iu.Insert(user);
iu.GetUser(1);
IDepartment iD = factory.CreateDepartment();
iD.Insert(department);
iD.GetDepartment(1);
只有一个User类和User操作类的时候,是只需要工厂方法模式的,但如果数据库有多个表,而SQL Server与Access又是两大不同的分类,解决这种涉及到多个产品系列的问题,就可以用抽象工厂模式。
抽象工厂模式
抽象工厂模式(Abstract Factory),提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
AbstractProductA和AbstractProductB是两个抽象产品,它们都有两种不同的实现,就前面的例子来说就是User和Department,而ProductA1、ProductA2和ProductB1、ProductB2就是对两个抽象产品的具体分类的实现,比如ProductA1可以理解为是SqlserverUser,而ProductB1是AccessUser。
IFactory是一个抽象工厂模式,它里面包含所有产品创建的抽象方法。而ConcreteFactory1和ConcreteFactory2就是具体的工厂了。就像SqlserverFactory和AccessFactory一样。
通常是在运行的时候再创建一个ConcreteFactory类的实例,这个具体的工厂再创建具体特定实现的产品对象,也就是说,为创建不同的产品对象,客户端应使用不同的具体工厂。
抽象工厂模式的优点与缺点
最大的好处是易于交换产品系列,由于具体工厂类,例如IFactory factory = new AccessFactory(),在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。
第二大好处是,它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口来操作实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户代码中。
抽象工厂模式可以很方便的切换两个数据库的访问,但如果你的需求来自增加功能,比如我们现在要增加项目表Project,就要增加三个类,Iproject、SqlserverProject、AccessProject,还需要修改IFactory、SqlserverFactory和AccessFactory才行。
假设很多地方都在使用IUser和IDepartment,在每一个类的开始都需要声明IFactory factory = new SqlserverFactory (),如果有100个类调用,那就要改100才代码,才能换成AccessFactory。编程是门艺术,这样大批量的改动,显然是非常丑陋的做法。
用简单工厂来改进抽象工厂
去除IFactory、SqlserverFactory和AccessFactory三个工厂,取而代之的是DataAccess类,用一个简单工厂模式来实现。
class DataAccess
{
//private static readonly string db = "Sqlserver";
private static readonly string db = "Access"; // 数据库名称
public static IUser CreateUser()
{
IUser iuser = null;
switch (db)
{
case "Sqlserver":
iuser = new SqlserverUser();
break;
case "Access":
iuser = new AccessUser();
break;
}
return iuser;
}
public static IDepartment CreateDepartment()
{
IDepartment department = null;
switch (db)
{
case "Sqlserver":
department = new SqlserverDepartment();
break;
case "Access":
department = new AccessDepartment();
break;
}
return department;
}
}
客户端代码
User user = new User();
Department department = new Department();
IUser iu = DataAccess.CreateUser();
iu.Insert(user);
iu.GetUser(1);
IDepartment iD = DataAccess.CreateDepartment();
iD.Insert(department);
iD.GetDepartment(1);
客户端只需要DataAccess.CreateUser()和DataAccess.CreateDepartment()来生成具体的数据库访问类实例,客户端没有出现任何一个SQL Server或Access的字样,达到了解耦的目的。
但是,如果需要增加Oracle数据库访问,本来抽象工厂只增加一个OracleFactory工厂类就可以了,现在就需要在DataAccess类中的每个方法的switch中加case了。
用反射+抽象工厂的数据访问数据
如果可以不在程序里写明‘如果是SqlServer就去实例化SQL Server数据库相关类,如果是Access就去实例化Access相关类这样的语句’,而是根据字符串db的值去某个地方找应该要实例化的类是哪一个。这样,就可以对switch说再见了。
反射:
Assembly.Load(“程序集名称”).CreateInstance(“命名空间.类名称”)
需要using System.Reflection;来引用Reflection。
有了反射,我们获得实例可以用下面两种写法。
// 常规的写法
IUser result = new SqlserverUser();
// 反射的写法
using System.Reflection;
IUser result = (IUser)Assembly.Load("抽象工厂模式").CreateInstance("抽象工厂模式.SqlserverUser");
原来的实例化是在编译时写死在代码里,需要更换为AccessUser,就需要修改代码重新编译。用了反射之后,将程序由编译时转为运行时。由于‘CreateInstance("抽象工厂模式.SqlserverUser")’中的字符串是可以写成变量的,而变量的值是可以动态改变的,所以就去除了switch判断的麻烦。
DataAccess类,用反射技术,取代IFactory、SqlserverFactory和AccessFactory。
class DataAccess
{
private static readonly string AssemblyName = "抽象工厂模式"; // 程序集名称
private static readonly string db = "Sqlserver"; // 数据库名称,可替换成Access。
public static IUser CreateUser()
{
string className = AssemblyName + "," + db + "User";
return (IUser)Assembly.Load(AssemblyName).CreateInstance(className);
}
public static IDepartment CreateDepartment()
{
string className = AssemblyName + "," + db + "Department";
return (IDepartment)Assembly.Load(AssemblyName).CreateInstance(className);
}
}
Unity中反射的使用
下面将DataAccess的函数返回实例代码改为Unity反射,假设已知类型名称。
using System.Reflection; // 引入反射
using System;
//return (IUser)Assembly.Load(AssemblyName).CreateInstance(className);
return (IUser)Activator.CreateInstance(Type.GetType(db + "User")); // Unity反射的实现
//return (IDepartment)Assembly.Load(AssemblyName).CreateInstance(className);
return (IDepartment)Activator.CreateInstance(Type.GetType(db + "Department")); // Unity反射的实现
1.取得数据类型Type
方式一:Type.GetType(“类型全名”);
适合于类型的名称已知
方式二:obj.GetType();
适合于类型名未知,类型未知,存在已有对象
方式三:typeof(类型)
适合于已知类型
方式四:Assembly.Load(“XXX”).GetType(“名字”);
适合于类型在另一个程序集中
如果我们增加了Oracle数据访问,增加了相关的类之后,就只需要将db改为OracleUser就能变成OracleUser的实例了。
如果要增加Project产品,只需要增加三个与Project相关的类,再修改DataAccess,增加一个CreateProject()方法就可以了。
但是有个问题,在更换数据库访问时,还是需要去改程序(修改db的值)重编译。
用反射+配置文件实现数据访问程序
通过读文件来给db字符串赋值,在配置文件中写明是Sqlserver还是Access,这样就连DataAccess类也不用改了。
所有在用简单工厂的地方,都可以考虑用反射来去除switch或if,解除分支判断带来的耦合。