第15章 就不能换DB吗?—抽象工厂模式

由于抽象工厂在我们编程当中经常使用和常见,所有本篇文章对《大话设计模式》中的15章做了很详细的比较。通过一个Dao层可以更换访问任意数据库的例子来学习抽象工厂模式。例如:Dao层可以访问Sqlserver数据库,也可以访问Access数据库,当程序新增访问Oracle数据库时,无需修改现有代码,只需要添加访问Oracle相关的类就可以,实现了开闭原则。本篇文章的例子中每种数据库上都有User和Department表,我们Dao层对这两个表进行查询和插入操作。

最基本数据库访问

一下是访问Sqlserver数据库的代码。

public class User {
	private int id;
	private String name;
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}
public class SqlserverUser{

	public void insert(User user) {
		System.out.println("SqlserverUser insert user");
	}

	public void getUser(int id) {
		System.out.println("SqlserverUser get user");
	}

}
public class ClientTest {
	public static void main(String [] args){
		SqlserverUser su = new SqlserverUser();
		su.getUser(1);
		su.insert(new User());
	}
}

在上面的代码中,如果我们现在要更换Access数据库,那简直是灾难性的改动。主要是因为new Sqlserver()被硬编码了,访问数据库User表的类Sqlserver框死在了su变量上,也就说客户端依赖了具体的SqlserverUser类。如果灵活一点的那就是多态,在实际运行时决定使用SqlserverUser,还是AccessUser。这里我们使用工厂方法来解决这问题,让工厂方法模式来封装new Sqlserver()所造成的变化。

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

我们可以把访问User表的类看做是抽象产品,让客户端依赖于抽象,而不是依赖具体的user表访问类。访问Sqlserver数据库User表的SqlserverUser类和访问Access数据库User表的AccessUser类看做是具体产品。有一个抽象工厂和一个生产SqlserverUser具体产品,一个用于生产AccessUser具体产品。

uml图

第15章 就不能换DB吗?—抽象工厂模式_第1张图片

代码实现

IUser接口,抽象产品,用于客户端和具体的数据库访问解耦。
public interface IUser {
	void insert(User user);
	void getUser(int id);
}

SqlserverUser实现了IUser接口的具体产品。访问Sqlserver数据库user表的类。
public class SqlserverUser implements IUser {

	@Override
	public void insert(User user) {
		System.out.println("SqlserverUser insert user");
	}

	@Override
	public void getUser(int id) {
		System.out.println("SqlserverUser get user");
	}

}

AccessUser实现了IUser接口的具体产品。访问Access数据库user表的类。
public class AccessUser implements IUser {

	@Override
	public void insert(User user) {
		System.out.println("AccessUser insert user");
	}

	@Override
	public void getUser(int id) {
		System.out.println("AccessUser get user");
	}

}

IFactory接口,抽象工厂,定义了一个创建访问User表对象的抽象工厂接口。
public interface IFactory {
	IUser createUser();
}

SqlserverUser实现了IFactory接口的具体工厂,用于生产访问Sqlserver数据库表的相关类。
public class SqlserverFacotry implements IFactory {

	@Override
	public IUser createUser() {
		// TODO Auto-generated method stub
		return new SqlserverUser();
	}

}

Access实现了IFactory接口的具体产品,用于生产访问Access数据库表的相关类。
public class AccessFactory implements IFactory {

	@Override
	public IUser createUser() {
		// TODO Auto-generated method stub
		return new AccessUser();
	}

}

客户端
public class ClientTest {
	public static void main(String [] args){
		
		IFactory factory = new SqlserverFacotry();
		//IFactory factory = new AccessFactory();
		IUser iu = factory.createUser();
		iu.insert(new User());
		iu.getUser(1);

	}
}

现在用工厂方法来实现,如果我们从访问Sqlserver数据库切换到Access数据库,只需要把new SqlserverFactory()换成new AccessFactory()即可,由于多态的关系IUser iu实现根本不知道是在访问那个具体的数据库,只有在运行时才知道,这就是所谓的业务逻辑与数据库访问的解耦。在访问数据库变更的情况下,我们用相对较小的改动实现这种变更。

使用抽象工厂模式的数据库访问程序

我们提出一个问题:Sqlserver和Access数据库中不可能只有User一个表,可能还有其它的表,比如增加部门表(Department),那么我们应该怎么办呢?

uml图

第15章 就不能换DB吗?—抽象工厂模式_第2张图片
我们看出我们需改动的地方如下:
1,在IFacotry接口中增加creatDepartment()方法,SqlserverFactory和AccessFactory分别实现createDepartemnt()方法。
2,增加操作部门表的抽象,IDepartment接口。
3,增加SqlserverDepparment和AccessDepartment类,分别实现IDepartment接口。
如果是增加一个对Oracle数据库的访问,改动如下:
1,增加OracleFactory类,用于生产操作oracle表的类,实现IFactory中的所有方法。
2,增加OracleUser类,用于操作user表,实现IUser中所有的方法。
3,增加OracleDepartment,用于操作department表,实现IDepartment中所有方法。
其实我们通过需求的不断的演变重构出了抽象工厂模式。只有一个IUser抽象产品和IUser具体产品的时候,是只需要工厂方法模式。但现在显然数据库中有很多表,而Sqlserver和Access又是两种不同的分类,所有涉及到多个产品系列的问题,有一个专门的工厂模式叫做抽象工厂。我们可以这么理解,只创建单一产品的工厂叫做工厂方法,可以创建多个产品系列的工厂叫做抽象工厂。举个例子:富士康和可胜科技都是IPhone的代工厂,可生产IPhone6,富士康和可胜科技都是具体的工厂,富士康的IPhone6和可胜科技的IPhone6都是具体的超,这个时候是工厂方法模式。IPhone生产让苹果公司很满意,把IPhone6s的代工也交给了富士康和可胜,这个时候就是抽象工厂模式了。富士康和可胜都在不仅可以生产IPhone6产品了,也可以生产IPhone6s。

抽象工厂模式的优点与缺点

优点
1,最大的好处就是易于交换系列产品,那我们这个例子来说就我可以很容易的切换数据库连接。
2,让具体的创建实例过程与客户端分离,客户端通过抽象来操作他们的实例。客户端代码中也不会出现具体产品类,只会出现抽象产品接口。

缺点
1,如果我们的客户端有100个地方调用了IFactory factory = new SqlserverFactory();,在切换到Access数据库时,需要改动一百个地方。这不能实现只更改一处就实现数据库切换的目的。
2,我们需要增加一张表,还需要修改SqlserverFactory、AccessFactory和IFactory类,违背了开闭原则。

用简单工厂来改进抽象工厂解决缺点1

去除IFactory、SqlserverFactory、AccessFactory,取而代之的是简单工厂AccessData类。代码示例中只给出了AccessData和ClientTest的代码,产品相关代码没有变化。如下:
public class DataAccess {

	private static String DB = "sqlserver";
	
	public static IUser createUser(){
		IUser user = null;
		switch (DB) {
			case "sqlserver":
				user = new SqlserverUser();
				break;
			case "access":
				user = new AccessUser();
				break;
		}
		
		return user;
	}
	
	public static IDepartment createDepartment(){
		IDepartment dep = null;
		
		switch (DB) {
			case "sqlserver":
				dep = new SqlserverDepartment();
				break;
			case "access":
				dep = new AccessDepartment();
				break;
		}
		
		return dep;
	}
	
}
public class DataAccess {

	private static String DB = "sqlserver";
	
	public static IUser createUser(){
		IUser user = null;
		switch (DB) {
			case "sqlserver":
				user = new SqlserverUser();
				break;
			case "access":
				user = new AccessUser();
				break;
		}
		
		return user;
	}
	
	public static IDepartment createDepartment(){
		IDepartment dep = null;
		
		switch (DB) {
			case "sqlserver":
				dep = new SqlserverDepartment();
				break;
			case "access":
				dep = new AccessDepartment();
				break;
		}
		
		return dep;
	}
	
}
简单工厂方这种方式的改进让客户端不再依赖new SqlserverFactory()或new AccessFactory(),而是仅仅依赖于AccesData简单工厂类,让客户端与具体的工厂类解耦。将需要访问的数据库以成员变量的方式声明在AccessData中,修改变量值即可达到切换数据库访问的目的,不需要客户端传给简单工厂中的方法。这样做解决抽象工厂模式的缺点1。
虽然摒弃了IFactory、SqlserverFactory、AccessFactory类。但是我们在新增一个表时会在AccessData类总增加相应的方法。新增一个数据库时需要在每个方法添加case “oracle” 语句。还是没有解决抽象工厂模式的缺点2。

反射技术+抽象工厂来改进缺点2

我们现在的的AccessData类时这样做的:使用switch case来判读,如果是sqlserver,就创建并返回sqlserver数据库相关的类,如果是access数据库,就创建并返回access数据库相关的类。如果我们能根据String类型的变量值去找数据库相关的类就好了。实现如下:
这是程序的包结构。
第15章 就不能换DB吗?—抽象工厂模式_第3张图片


代码实现:
package fly.zxy.dhms.chapter15_8.factory;

import fly.zxy.dhms.chapter15_8.IDB.IDepartment;
import fly.zxy.dhms.chapter15_8.IDB.IUser;

public class DataAccess {

	private static String DB = "sqlserver";
	private static String packageBasePath = "fly.zxy.dhms.chapter15_8";
	
	public static IUser createUser(){
		String name = "User";
		IUser iu =null;
		iu = (IUser) ref( getPackagePath(name) );
		return iu;
	}
	
	public static IDepartment createDepartment(){
		String name = "Department";
		IDepartment dep = null;
		dep = (IDepartment) ref( getPackagePath(name) );
		return dep;
	}
	
	private static String getPackagePath(String className){
		//fly.zxy.dhms.chapter15_8.sqlserverDB
		//fly.zxy.dhms.chapter15_8.accessDB
		className = DB.substring(0,1).toUpperCase() + DB.substring(1, DB.length()) + className;
		String path = packageBasePath+"."+DB+"DB"+"."+className;
		return path;
	}
	
	private static Object ref(String className){
		Object obj = null;
		try {
			Class<? extends Object> cls = Class.forName(className);
			obj = cls.newInstance();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InstantiationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return obj;
	}
}
由于使用了反射技术,我们在增加表的时候只需要在AccessData类中增加对应的方法,而方法中没有了一堆的switch case分支判断。新增对一种数据库的访问,无需再更改每个方法增加case语句了。解决了抽象工厂模式的缺点二。其实从某种角度来说,所有在使用简单工厂的地方,都可以考虑用反射来去除switch或if。
我们发现还有点小瑕疵,我们需要修改DB变量的值类来实现数据库的切换,这样还是需要改动代码,需要重新编译。这个小瑕疵可以使用配置文件来搞定,就是讲DB变量的值从XML文件或properties文件中读取出来。












你可能感兴趣的:(抽象工厂,反射,配置文件,工厂方法)