由一个例子引出抽象工厂模式:
在实际开发中,我们有时会遇到切换数据库的需求,由于数据库之间的差异,不同数据库对数据表的增删改查操作也有差异。为了解决这个问题,我们很容易想到用工厂模式来实现:
User类:
public class User {
private Integer id;
// get set 方法...
}
IUser接口:用于客户端访问,解除与具体数据库访问的耦合。
public interface IUser {
void insert(User user);
User getUser(int id);
}
SqlServerUser类:用于访问SqlServer的User。
public class SqlServerUser implements IUser {
@Override
public void insert(User user) {
System.out.println("sql server 数据库新增用户记录");
}
@Override
public User getUser(int id) {
System.out.println("sql server 数据库获取用户记录");
return null;
}
}
MySqlUser类:用于访问MySQL的User。
public class MySqlUser implements IUser {
@Override
public void insert(User user) {
System.out.println("mysql 新增用户");
}
@Override
public User getUser(int id) {
System.out.println("mysql 获取用户");
return null;
}
}
IFactory接口:定义一个创建访问User表对象的抽象工厂接口。
public interface IFactory {
/**
* 创建用户增删改查的实现类
* @return
*/
IUser createUser();
}
SqlServerFactory类:实现IFactory接口,实例化SqlServerUser。
public class SqlServerFactory implements IFactory {
@Override
public IUser createUser() {
return new SqlServerUser();
}
}
MySqlFactory类:实现IFactory接口,实例化MySqlUser。
public class MySqlFactory implements IFactory {
@Override
public IUser createUser() {
return new MySqlUser();
}
}
客户端代码:
public class AbstractFactoryTest {
public static void main(String[] args) {
IFactory factory = new SqlServerFactory();
// 如果需要切换MySQL数据库,只需要将上面的代码换成下面的即可
// IFactory factory = new MySqlFactory();
IUser user = factory.createUser();
user.insert(new User());
user.getUser(1);
}
}
输出结果:
sql server 数据库新增用户记录
sql server 数据库获取用户记录
现在如果要换数据库,只需要将new SqlServerFactory()改成new MySqlFactory()即可,这就实现了业务逻辑与数据访问的解耦。
这时如果要增加一个部门表(Department表)此时应该怎么办呢?这时我们需要增加几个类:
Department类:
public class Department {
private Integer id;
}
IDepartment接口:用于客户端访问,解除与具体数据库访问的耦合:
public interface IDepartment {
void insert(Department department);
void getDepartment(int id);
}
SqlServerDepartment类:用于访问SqlServer的Department。
public class SqlServerDepartment implements IDepartment {
@Override
public void insert(Department department) {
System.out.println("SqlServer新增 Department 记录");
}
@Override
public void getDepartment(int id) {
System.out.println("SqlServer获取 Department 记录");
}
}
MySqlDepartment类:用于访问MySql的Department。
public class MySqlDepartment implements IDepartment {
@Override
public void insert(Department department) {
System.out.println("MySQL新增 Department 记录");
}
@Override
public void getDepartment(int id) {
System.out.println("MySQL获取 Department 记录");
}
}
IFactory接口:定义一个创建访问Department表对象的抽象工厂类接口。
public interface IFactory {
/**
* 创建用户增删改查的实现类
* @return
*/
IUser createUser();
// 增加的接口方法
IDepartment createDepartment();
}
SqlServerFactory类:实现IFactory接口,实例化SqlServerUser和SqlServerDepartment。
public class SqlServerFactory implements IFactory {
@Override
public IUser createUser() {
return new SqlServerUser();
}
@Override
public IDepartment createDepartment() {
return new SqlServerDepartment();
}
}
MySqlFactory类:实现IFactory接口,实例化MySqlUser和MySqlDepartment。
public class MySqlFactory implements IFactory {
@Override
public IUser createUser() {
return new MySqlUser();
}
@Override
public IDepartment createDepartment() {
return new MySqlDepartment();
}
}
客户端代码:
public class AbstractFactoryTest {
public static void main(String[] args) {
IFactory factory = new SqlServerFactory();
// 如果需要切换MySQL数据库,只需要将上面的代码换成下面的即可
// IFactory factory = new MySqlFactory();
IUser user = factory.createUser();
user.insert(new User());
user.getUser(1);
IDepartment department = factory.createDepartment();
department.insert(new Department());
department.getDepartment(1);
}
}
输出:
sql server 数据库新增用户记录
sql server 数据库获取用户记录
SqlServer新增 Department 记录
SqlServer获取 Department 记录
这样就实现增加一个Department的操作了。只有一个User类和User操作类的时候,是只需要工厂方法模式的,但现在显然数据库中有很多的表,而SQL server与MySQL又是两大不同的分类,所以解决这种涉及到多个产品系列的问题,有一个专门的工厂模式叫做抽象工厂模式。
抽象工厂模式:提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类。
通常在运行的时刻再创建一个工厂类的实例,这个具体的工厂再创建具有特定实现的产品对象,也就是说,为创建不同的产品对象,客户端应使用不同的具体工厂。
抽象工厂模式的优点与缺点
工厂模式的最大好处就是易于交换产品系列,由具体工厂类,例如IFactory factory = new SqlServerFactory(),在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,他只需要改变具体工厂即可使用不同的配置。
第二大好处是,它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操纵实例,产品具体类名也被具体工厂的实现分离,不会出现在客户端代码中。就像刚才的例子,客户端所认识的只有IUser和IDepartment,至于使用SQL server实现还是MySQL实现就不知道了。
当然,抽象工厂模式也有缺点,比如要增加一个Department表就要增加很多类,IDepartment,SqlServerDepartment,MySqlDepartment,还需要更改IFactory、SqlServerFactory和MySqlFactory才可以完全实现。而且我们的客户端程序不止一个,很多地方都会用到IUser和IDepartment,如果100个地方要切换数据库就要改动100处代码,如此大批量的改动显然是非常丑陋的做法。
用简单工厂来改进抽象工厂
去除IFactory、SqlServerFactory和MySqlFactory三个工厂类,取而代之的是DataAccess类,用一个简单工厂实现:
public class DataAccess {
private final static String DB = "MySql";
// private final static String db = "SqlServer";
public static IUser createUser(){
IUser result = null;
switch (DB){
case "MySql":
result = new MySqlUser();
break;
case "SqlServer":
result = new SqlServerUser();
break;
default:
break;
}
return result;
}
public static IDepartment createDepartment(){
IDepartment result = null;
switch (DB){
case "MySql":
result = new MySqlDepartment();
break;
case "SqlServer":
result = new SqlServerDepartment();
break;
default:
break;
}
return result;
}
}
客户端代码:
public class SimpleFactoryTest {
public static void main(String[] args) {
IUser user = DataAccess.createUser();
user.insert(new User());
user.getUser(1);
IDepartment department = DataAccess.createDepartment();
department.insert(new Department());
department.getDepartment(1);
}
}
客户端没有出现一个SqlServer或者Mysql的字样,实现了完全解耦。
上面的改动还存在不足之处,比如增加一个Oracle数据库访问,本来抽象工厂增加一个OracleFactory工厂类就可以了,现在就比较麻烦了,需要在DataAccess类的每个switch语句中加case。
反射+抽象工厂的数据访问程序
我们可以通过反射进一步优化代码,去除switch语句:
改变后的DataAccess:
public class DataAccess {
private final static String BASE_PACKAGE_PATH = "com.nss.abstractfactory";
private final static String DB = "MySql";
// private final static String DB = "SqlServer";
public static IUser createUser() throws Exception {
String className = BASE_PACKAGE_PATH + "." + DB + "User";
return (IUser) Class.forName(className).newInstance();
}
public static IDepartment createDepartment() throws Exception {
String className = BASE_PACKAGE_PATH + "." + DB + "Department";
return (IDepartment) Class.forName(className).newInstance();
}
}
我们只需要事先指定需要创建的类的路径就可以动态获取到相应的内容,现在我们增加一个Oracle数据访问,相关类的增加是避免不了的,我们对于扩展是开放的,就目前而言,只需要将DB改为Oracle并增加相应的OracleUser以及OracleDepartment就可以了。
如果要增加Project产品时,只需要增加三个与Project相关的类,再修改DataAccess,在其中增加一个public static IProject createProject()方法就可以了。当然,关于DB的配置我们也可以放在配置文件中读取,这样代码就不会做任何修改了。
所有使用简单工厂的地方,都可以考虑用反射技术来去除switch或if,解除分支判断带来的耦合。