抽象工厂模式(Abstract Factory):提供一个创建一系列相关或互相依赖对象的的接口,而无需指定它们具体的类。
下面用更换数据库的例子来详细讲解抽象工厂模式
比如说我们本来用的是SQL Server来存储数据,因为特殊要求要更换成Oracle,这时候就需要改动我们原本写好的代码了,但学习完抽象工厂模式之后能够使这种改动最小化。
首先,下面先用工厂方法模式实现一遍,以便于看出抽象工厂模式的不同之处
UML图如下
我们以 ‘新增用户’ 和 ’获取用户‘ 这两个功能为例,对 User 表进行操作
User表对应的类如下
public class User {
private int uid;
private String uname;
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public String getUname() {
return uname;
}
public void setUname(String uname) {
this.uname = uname;
}
}
接下来是操作user表的接口,也就是UML图右上方的interface
public interface IUser {
public void insert(User user);
public User getUser(int uid);
}
SqlserverUser类,用来操作SQLserver中的user表
public class SqlserverUser implements IUser{
public void insert(User user){
System.out.println("在SQL Server中的user表中插入一条元素");
}
public User getUser(int id){
System.out.println("在SQL Server中的user表得到id为"+id+"的一条数据");
return null;
}
}
OracleUser类,用来操作Oracle中的user表
public class OracleUser implements IUser{
@Override
public void insert(User user) {
System.out.println("在oracle中的user表中插入一条元素");
}
@Override
public User getUser(int uid) {
System.out.println("在oracle中的user表得到id为"+uid+"的一条数据");
return null;
}
}
接下来是工厂接口
public interface IFactory {
public IUser createUser(); //用于操作User表的对象
}
实现了接口的具体工厂类,用来实例化 SqlserverUser
public class SqlserverFactory implements IFactory {
@Override
public IUser createUser() {
return new SqlserverUser(); //操作SQL Server中User表的对象
}
}
实现了接口的具体工厂类,用来实例化OracleUser
public class oracleFactory implements sqlFactory {
@Override
public IUser createUser() {
return new oracleUser(); //操作Oracle中User表的对象
}
}
客户端代码
public class Client {
public static void main(String[] args) {
User user = new User;
IFactory factory = new SqlserverFactory(); //如果要更改数据库,只需要修改这一语句
IUser iu = factory.createUser();
iu.insert(user);
iu.getUser(1);
}
}
此时如果要更换数据库,只需要把客户端代码中的 new SqlsercerFactory() 改成 new OracleFactory()就ok了,此时操作user表的对象 iu 并不清楚在使用哪个数据库,它只管调用工厂的方法实例化自己,然后完成工作,如此便实现了业务逻辑与数据访问的解耦。
那抽象工厂模式到底又有什么不同呢?
我们的数据库里面不能只有一个user表吧,肯定会有其他的一些表,假设我们现在要增加一个部门表(Department)
public class Department {
private int id;
private String deptName;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Date getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
}
我们就得修改代码了,这次我们用抽象工厂模式
UML结构图如下
此时就要新增一些类了,首先是IDepartment接口,用于客服端访问,解除与具体数据库的耦合
public interface IDepartment {
public void insert(Department department);
public Department getDepartment(int id);
}
SqlserverDepartment类,用来访问 SQL Server 的 Department
public class SqlserverDepartment implements IDepartment{
public void insert(Department department) {
System.out.println("对 SQL Server 里的 Department 表插入了一条数据");
}
public Department getDepartment(int id) {
System.out.println("通过 id 在 SQL Server 里的 Department 表得到了一条数据");
return null;
}
}
OracleDepartment类,用于访问 Oracle 的 Department
public class OracleDepartment implements IDepartment{
public void insert(Department department) {
System.out.println("对 Oracle 里的 Department 表插入了一条数据");
}
public Department getDepartment(int id) {
System.out.println("通过 id 在 Oracle 里的 Department 表得到了一条数据");
return null;
}
}
IFactory接口,生成具体对象的工厂的抽象接口
public interface IFactory {
public IUser createUser();
public IDepartment createDepartment(); //新增的方法,其他地方不变
}
SqlserverFactory类,用于生成 SqlserverUser 和 SqlserverDepartment 对象
public class SqlserverFactory implements IFactory{
public IUser createUser() {
return new SqlserverUser();
}
public IDepartment createDepartment() { //新增的 SqlserverDepartment 工厂
return new SqlserverDepartment();
}
}
OracleFantory类,用于生成 OracleUser 和 OracleDepartment 对象
public class OracleFantory implements IFactory{
public IUser createUser() {
return new OracleUser();
}
public IDepartment createDepartment() { //新增的 OracleDepartment 工厂
return new OracleDepartment();
}
}
客户端代码如下
public class Client {
public static void main(String[] args){
User user=new User();
Department department = new Department();
IFactory factory = new SqlserverFactory(); //更换数据库则更改此处
IUser iu = factory.createUser(); //与具体的数据库解耦,由工厂来返回相应的操作类
iu.getUser(1);
iu.insert(user);
IDepartment iDep = factory.createDepartment(); //与具体的数据库解耦
iDep.insert(department);
iDep.getDepartment(1);
}
}
至此,我们已经完成了一个抽象工厂模式的构建,只需要将客户端类中的 IFactory factory = new SqlserverFactory(); 改为 IFactory factory = new OracleFactory(); 就可以实现数据库的切换
小结
通过对比UML图,蓝色圈内是新增的内容,我们可以发现抽象工厂可以生产不止一个产品类,每一个具体的工厂对应一系列产品的创建
优点:
抽象工厂模式最大的好处是易于交换产品系列,由于具体工厂类,例如 IFactory factory=new OracleFactory(); 在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。不管是任何人的设计都无法去完全防止需求的更改,或者项目的维护,那么我们的理想便是让改动变得最小、最容易,例如我现在要更改以上代码的数据库访问时,只需要更改具体的工厂即可。
抽象工厂模式的另一个好处就是它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操作实例,产品实现类的具体类名也被具体的工厂实现类分离,不会出现在客户端代码中。就像我们上面的例子,客户端只认识IUser和IDepartment,至于它是MySQl里的表还是Oracle里的表就不知道了。
缺点:
如果要增加功能,比如上面的增加Department表,就有点太繁琐了。首先需要增加 IDepartment,SqlserverDepartment,oracleDepartment三个类。 然后我们还要去修改三个工厂类: IFactory, SqlserverlFactory, OracleFactory 才可以实现。
还有就是,客户端程序肯定不止一个,每次都需要声明IFactory factory=new SqlserverFactory(), 如果有100个调用数据库的类,就需要更改100次sqlFactory factory=new OracleFactory()。
这样大刀阔斧的改动,显然不是我们希望看到的情况,这时候我们就要想办法改进我们的代码
这是一种改进方案,去除IFactory、SqlserverFactory、OracleFactory三个工厂类,用一个DataAccess类取而代之,类似于简单工厂模式
UML图:
public class DataAccess {
private static String db="Sqlserver"; // 数据库名称,可更换成Oracle
// private static String db="Oracle";
public static IUser createUser(){
IUser result=null;
switch (db){ //由于db事先设置好了,所以此处根据db实例化相应的对象
case "Sqlserver":
result=new SqlserverUser();
break;
case "Oracle":
result=new OracleUser();
break;
}
return result;
}
public static IDepartment createDepartment(){
IDepartment result=null;
switch (db){
case "Sqlserver":
result=new MysqlDepartment();
break;
case "Oracle":
result=new OracleDepartment();
break;
}
return result;
}
}
客户端代码
public class Client {
public static void main(String[] args){
User user=new User();
Department department = new Department();
IUser iu= DataAccess.createUser(); //与数据库解耦,直接得到想要的对象
iu.getUser(1);
iu.insert(user);
IDepartment idept=EasyFactory.createDepartment();
idept.insert(department);
idept.getDepartment(1);
}
}
经过改进之后,代码看起来比之前更加i精简了,客户端已经不受改动数据库的影响了,因为我们们在DataAccess 类中已经设置好了 db 的值,所以客户端不会出现一个Sqlserver 或 Oracle 的字样。
但是,如果要增加功能,现在就要在 DataAccess 中的每一个 switch 中加 case 了。
既然这样,我们就该考虑能不能不使用switch case 语句来决定实例化哪个对象
我们原本的代码是在switch写明,根据db是什么来实例化相应的类,但是使用了反射技术后,程序就会根据db的值自己去寻找要实例化的类。
public class DataAccess {
private static String packName = "DesignPattern.abstractFactory"; //包名
private static String db = "Sqlserver"; //数据库名称
public static IUser createUser() throws Exception{
String className = packName+"."+db+"User"; //通过db来构建一个需要实例化的类的路径
return (IUser)Class.forName(className).newInstance(); //利用反射技术实例化类
}
public static ILogin createDepartment() throws Exception{
String className = packName+"."+db+"Department";
return (IDepartment)Class.forName(className).newInstance();
}
}
如此,我们便完成了使用反射技术的抽象工厂的改进工作
差别:
原来的实例化都是写死在 switch case 语句中的,使得代码看起来非常臃肿,并且修改起来的工作量也比较大。现在用了反射技术就可以用字符串来实例化对象,并且通过更换变量来控制要实例化的类,代码精简了不少。
专业一点来说:我们将程序由编译时变成了运行时。
比起之前的代码,现在这个版本可以说是美观了许多,可是我们需要更改数据库的时候还是要更改 DataAccess 类中 db 这个字符串的值,这使得程序又要重新编译一次,对于比较大的系统,编译一次的时间可是很久的,有没有办法改进呢?
这个问题可以用配置文件来解决
也就是程序通过读外部的文件来给字符串 db 赋值,在配置文件中写明是 Sqlserver 还是 Oracle,这样我们就连 DataAcces 类都不用改了
添加一个xml配置文件
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="db" value="Sqlserver"/>
</appSettings>
</configuration>
然后再更改 DataAccess类中 db 的赋值代码就ok了
到这里,整个程序已经比较完美了,我们用反射+抽象工厂模式+配置文件使得数据库访问程序变得维护性与扩展性都不错。