抽象工厂模式

思考并回答以下问题:

最基本的数据访问程序

原来的数据访问的做法,用‘新增用户’和‘得到用户’为例。

用户类,假设只有ID和Name两个字段,其余省略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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表,假设只有“新增用户”和“得到用户”方法,其余方法以及具体的SQL语句省略。

1
2
3
4
5
6
7
8
9
10
11
12
13
class SqlserverUser
{
public void Insert(User user)
{
Console.WriteLine("在SQL Server中给User表增加一条记录");
}

public User GetUser(int id)
{
Console.WriteLine("在SQL Server中根据ID得到User表一条记录");
return null;
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Program
{
static void Main(string[] args)
{
User user = new User();

SqlserverUser su = new SqlserverUser(); // 与Sql Server耦合

su.Insert(user); // 插入用户

su.GetUser(1); // 得到ID为1的用户

Console.Read();
}
}

这里之所以不能换数据库,原因就在于SqlserverUser su = new SqlserverUser()使得su这个对象被框死在SQL Server上了。如果这里是灵活的,专业点的说法,是多态的,那么在执行‘su.Insert(user);’和‘su.GetUser(1);’时就不用考虑是在用SQL Server还是在用Access。

下面用‘工厂方法模式’来封装new SqlserverUser()所造成的变化。

工厂方法模式是定义一个用于创建对象的接口,让子类决定实例化哪一个类。

用了工厂方法模式的数据访问程序

代码结构图

IUser接口,用于客户端访问,解除与具体数据库访问的耦合。

1
2
3
4
5
6
interface IUser
{
void Insert(User user);

User GetUser(int id);
}

SqlserverUser类,用于访问SQL Server的User。

1
2
3
4
5
6
7
8
9
10
11
12
13
class SqlserverUser : IUser
{
public void Insert(User user)
{
Console.WriteLine("在Sqlserver中给User表增加一条记录");
}

public User GetUser(int id)
{
Console.WriteLine("在Sqlserver中根据ID得到User表一条记录");
return null;
}
}

AccessUser类,用于访问Access的User。

1
2
3
4
5
6
7
8
9
10
11
12
13
class AccessUser : IUser
{
public void Insert(User user)
{
Console.WriteLine("在Access中给User表增加一条记录");
}

public User GetUser(int id)
{
Console.WriteLine("在Access中根据ID得到User表一条记录");
return null;
}
}

IFactory接口,定义一个创建访问User表对象的抽象的工厂接口。

1
2
3
4
interface IFactory
{
IUser CreateUser();
}

SqlServerFactory类,实现IFactory接口,实例化SqlserverUser。

1
2
3
4
5
6
7
class SqlServerFactory : IFactory
{
public IUser CreateUser()
{
return new SqlserverUser();
}
}

AccessFactory类,实现IFactory接口,实例化AccessUser。

1
2
3
4
5
6
7
class AccessFactory : IFactory
{
public IUser CreateUser()
{
return new AccessUser();
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Program
{
static void Main(string[] args)
{
User user = new User();
//AbstractFactory factory = new SqlServerFactory();
IFactory factory = new AccessFactory();

IUser iu = factory.CreateUser();

iu.Insert(user);
iu.GetUser(1);

Console.Read();
}
}

现在如果要换数据库,只需要把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
18
class 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
2
3
4
5
6
interface IDepartment
{
void Insert(Department department);

Department GetDepartment(int id);
}

SqlserverDepartment类,用于访问SQL Server的Department。

1
2
3
4
5
6
7
8
9
10
11
12
13
class SqlserverDepartment : IDepartment
{
public void Insert(Department department)
{
Console.WriteLine("在Sqlserver中给Department表增加一条记录");
}

public Department GetDepartment(int id)
{
Console.WriteLine("在Sqlserver中根据ID得到Department表一条记录");
return null;
}
}

AccessDepartment类,用于访问Access的Department。

1
2
3
4
5
6
7
8
9
10
11
12
13
class AccessDepartment : IDepartment
{
public void Insert(Department department)
{
Console.WriteLine("在Access中给Department表增加一条记录");
}

public Department GetDepartment(int id)
{
Console.WriteLine("在Access中根据ID得到Department表一条记录");
return null;
}
}

IFactory接口,定义一个创建访问Department表对象的抽象的工厂接口。

1
2
3
4
5
6
interface IFactory
{
IUser CreateUser();

IDepartment CreateDepartment();
}

SqlServerFactory类,实现IFactory接口,实例化SqlserverUser和SqlserverDepartment。

1
2
3
4
5
6
7
8
9
10
11
12
class SqlServerFactory : IFactory
{
public IUser CreateUser()
{
return new SqlserverUser();
}

public IDepartment CreateDepartment()
{
return new SqlserverDepartment();
}
}

AccessFactory类,实现IFactory接口,实例化AccessUser和AccessDepartment。

1
2
3
4
5
6
7
8
9
10
11
12
class AccessFactory : IFactory
{
public IUser CreateUser()
{
return new AccessUser();
}

public IDepartment CreateDepartment()
{
return new AccessDepartment();
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Program
{
static void Main(string[] args)
{
User user = new User();
Department dept = new Department();

//AbstractFactory factory = new SqlServerFactory();
IFactory factory = new AccessFactory();
IUser iu = factory.CreateUser();

iu.Insert(user);
iu.GetUser(1);

IDepartment id = factory.CreateDepartment();
id.Insert(dept);
id.GetDepartment(1);

Console.Read();
}
}

结果显示如下:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Program
{
static void Main(string[] args)
{
User user = new User();
Department dept = new Department();

IUser iu = DataAccess.CreateUser();

iu.Insert(user);
iu.GetUser(1);

IDepartment id = DataAccess.CreateDepartment();
id.Insert(dept);
id.GetDepartment(1);

Console.Read();
}
}

这里与其用那么多工厂类,不如直接用一个简单工厂来实现,抛弃了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
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="DB" value="Sqlserver"/>
</appSettings>
</configuration>

再添加引用System.configuration,并在程序开头增加using System.Configuration;,然后更改DataAccess类的字段DB的赋值代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Program
{
static void Main(string[] args)
{
User user = new User();
Department dept = new Department();

IUser iu = DataAccess.CreateUser();

iu.Insert(user);
iu.GetUser(1);

IDepartment id = DataAccess.CreateDepartment();
id.Insert(dept);
id.GetDepartment(1);

Console.Read();
}
}

现在我们应用了反射+抽象工厂模式解决了数据库访问时的可维护、可扩展的问题。从这个角度上说,所有在用简单工厂的地方,都可以考虑用反射技术来去除switch或if,解除分支判断带来的耦合。

switch或者if是程序里的好东西,但在应对变化上,却显得老态龙钟。反射技术的确可以很好地解决它们难以应对变化,难以维护和扩展的诟病。

0%