抽象工厂模式

在讲述这个模式之前,我们先看一个案例:模拟最基本的数据库访问:获取用户,向用户表插入记录

//用户类,假设只有ID和Name两个字段
public class User {
    private String id;
    private String name;
    
    //省略getter、setter方法    
}

//SqlserverUser类,用于操作User表,假设只有“新增用户”、“得到用户”,其余方法以及具体的SQL语句省略
public class SqlserverUser {
    public void insert(User user){
        System.out.println("在SQL  Server 中给User 表增加一条记录");
    }
    
    public User getUser(String id){
        System.out.println("在SQL  Server 中根据ID得到User表一条记录");
        return null;
    }
}

//测试方法
public class Test {
    public static void main(String[] args) {
        User user = new User();
        user.setId("1");
        user.setName("张三");
        
        SqlserverUser su = new SqlserverUser();
        //插入用户
        su.insert(user);
        
        //得到ID为1的用户
        User user2 = su.getUser("1");
    }
}

这里的SqlserverUser su = new SqlserverUser()使得su这个对象被框死在SQL Server上了,如果这里是灵活的,专业点的说法,是多态的,那么在执行su.insert(user)和su.getUser("1")的时候,就不用在意是哪一种数据库了。

现在我们用“工厂方法模式”来封装new SqlserverUser()所造成的变化。工厂方法模式是定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法模式的讲解请参考另一边文章:https://www.cnblogs.com/jwen1994/p/9970048.html

//IUser接口,解除与具体数据库访问的耦合
public interface IUser {
    void insert(User user);
    User getUser(String id);
}
//SqlserverUser类,用于访问SQL Server的User
public class SqlserverUser implements IUser{
    public void insert(User user){
        System.out.println("在SQL  Server 中给User 表增加一条记录");
    }
    
    public User getUser(String id){
        System.out.println("在SQL  Server 中根据ID得到User表一条记录");
        return null;
    }
}
//OracleUser类,用于访问Oracle的User
public class OracleUser implements IUser {
    public void insert(User user){
        System.out.println("在Oracle 中给User 表增加一条记录");
    }
    
    public User getUser(String id){
        System.out.println("在Oracle 中根据ID得到User表一条记录");
        return null;
    }

}

IUser接口,解除与具体数据库访问的耦合。那么使用哪个数据库又该怎么确定呢?这里创建 IUser的工厂类,用来创建具体的数据库实现类。

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

//SqlServerFactory类,实现IFactory接口,实例化SqlserverUser
public class SqlServerFactory implements IFactory {
    public IUser createUser() {
        return new SqlserverUser();
    }
}

//OracleFactory 类,实现IFactory接口,实例化OracleUser
public class OracleFactory implements IFactory{
    public IUser createUser() {
        return new OracleUser();
    }
}

测试方法

public class Test {
    public static void main(String[] args) {
        User user = new User();
        user.setId("1");
        user.setName("张三");
        //若要更改成Oracle数据库,只需要将本句改成IFactory factory = new OracleFactory();
        IFactory factory = new SqlServerFactory();
        
        IUser iu = factory.createUser();
        //插入用户
        iu.insert(user);
        
        //得到ID为1的用户
        User user2 = iu.getUser("1");
    }
}

现在如果要换数据库,只需要把new SqlServerFactory()改成new OracleFactory(),由于多态的关系,使得声明IUser接口的对象iu事先根本不知道是在访问哪个数据库,却可以在运行时很好地完成工作,这就是所谓的业务逻辑与数据访问的解耦。

现在还有两个问题:1、代码里需要声明new SqlServerFactory(),这里只是一处声明,如果有很多处,岂不是都需要更改? 2、数据库里不可能只有一张表,如果需要增加一张部门表(Department表),怎么办?

我们先处理第二个问题。

//部门类
public class Department {
    private String id;
    private String name;
    
    //省略getter、setter方法
}

//IDepartment接口,解除与具体数据库访问的耦合
public interface IDepartment {
    void insert(Department department);
    Department getDepartment(String id);
}

//SqlserverDepartment用于访问SQL  Server的Department
public class SqlserverDepartment implements IDepartment{
    public void insert(Department department){
        System.out.println("在SQL  Server 中给Department表增加一条记录");
    }
    public Department getDepartment(String id){
        System.out.println("在SQL  Server 中根据ID得到Department表一条记录");
        return null;
    }
}

//OracleDepartment 用于访问Oracle的Department
public class OracleDepartment implements IDepartment{
    public void insert(Department department){
        System.out.println("在Oracle 中给Department表增加一条记录");
    }
    public Department getDepartment(String id){
        System.out.println("在Oracle 中根据ID得到Department表一条记录");
        return null;
    }
}
public interface IFactory {
    IUser createUser();
    //增加接口方法
    IDepartment createDepartment();
}

public class SqlServerFactory implements IFactory {
    public IUser createUser() {
        return new SqlserverUser();
    }
    //增加SqlserverDepartment工厂
    public IDepartment createDepartment() {
        return new SqlserverDepartment();
    }
}

public class OracleFactory implements IFactory{
    public IUser createUser() {
        return new OracleUser();
    }
    //增加OracleDepartment工厂
    public IDepartment createDepartment() {
        return new OracleDepartment();
    }
}

测试方法

public class Test {
    public static void main(String[] args) {
        User user = new User();
        user.setId("1");
        user.setName("张三");
        
        Department dept = new Department();
        dept.setId("1");
        dept.setName("财务部");
        
        //只需确定实例化哪一个数据库访问对象给factory
        //IFactory factory = new SqlServerFactory();
        IFactory factory = new OracleFactory();
        
        //此时已与具体的数据库访问解除了依赖
        IUser iu = factory.createUser();
        //插入用户
        iu.insert(user);
        //得到ID为1的用户
        User user2 = iu.getUser("1");
        
        //此时已与具体的数据库访问解除了依赖
        IDepartment id = factory.createDepartment();
        id.insert(dept);
        Department dept2 = id.getDepartment("1");
    }
}

输出结果:

在Oracle 中给User 表增加一条记录
在Oracle 中根据ID得到User表一条记录
在Oracle 中给Department表增加一条记录
在Oracle 中根据ID得到Department表一条记录

看到这里,有的人会有疑问:这个例子中,哪些部分和抽象工厂模式有关呢?之前说的第一个问题还没解决呢!!!

如果你自己看了上面介绍工厂方法模式的文章,对比了两个例子,你会发现本文的例子中工厂接口IFactory定义了两个方法。难道这就是区别?

对,这就是区别!

抽象方法的定义:提供一个创建一系列相关或互相依赖对象的接口,而无需指定他们具体的类。

一系列相关或互相依赖对象:本例中它们都依赖数据库

无需指定他们具体的类:具体的实现类在子工厂中才会被创建出来。

这样做的好处是什么呢?

最大的好处便是易于交换产品系列,由于具体工厂类,例如IFactoiy factory = new OracleFactory(), 在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。我们的设计不能去防止需求的更改,那么我们的理想便是让改动变得最小,现在如果你要更改数据库访问,我们只需要更改具体工厂就可以做到。第二大好处是,它让具体的创建实例过程与main方法分离,main方法是通过它们的抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在main方法代码中。事实上,上面的例子,main方法所认识的只有lUser和IDepartment,至于它是用SQL Server来实现还是Oracle来实现就不知道了。

抽象工厂模式可以很方便地切换两个数据库访问的代码,但是如果需求来自增加功能,比如现在要增加项目表Project, 需要改动哪些地方?

至少要增加三个类,IProject、SqlserverProject、OracleProject,还需要更改IFactory、 SqlserverFactory和OracleFactory才可以完全实现。这非常糟糕。还有就是程序类显然不会是只有一个,有很多地方都在使用IUser或 IDepartment,而这样的设计,其实在每一个类的开始都需要声明IFactoiy fcctoiy = new SqlserverFactory(), 如果我有100个调用数据库访问的类,是不是就要更改100次IFactory factory = new OracleFactory()这样 的代码才行?这不能解决更改数据库访问时,改动一处就完全更改的要求。

为了解决第一个问题:我们得先了解Java的反射机制:https://www.cnblogs.com/jwen1994/p/10141460.html

用反射+抽象工厂的数据访问程序

DataAccess类,用反射技术,取代IFactory、SqlserverFactory和OracleFactory

public class DataAccess {
    private static final String packageName="com.jwen.model2";//包名
    private static final String db="Sqlserver";//哪种数据库
    //获取当前线程的类加载器
    private static ClassLoader loader = Thread.currentThread().getContextClassLoader();

    public static IUser createUser() throws Exception{
        //通过类加载器加载对象
        Class clazz = loader.loadClass(packageName+"."+db+"User");
        //获取类的默认构造器对象并通过它实例化类
        IUser iuser = (IUser) clazz.getDeclaredConstructor(null).newInstance();
        return iuser;
    }
    
    public static IDepartment createDepartment() throws Exception{
        //通过类加载器加载对象
        Class clazz = loader.loadClass(packageName+"."+db+"Department");
        //获取类的默认构造器对象并通过它实例化类
        IDepartment dept = (IDepartment) clazz.getDeclaredConstructor(null).newInstance();
        return dept;
    }
}

测试方法

public class Test {
    public static void main(String[] args) throws Exception {
        User user = new User();
        user.setId("1");
        user.setName("张三");
        
        Department dept = new Department();
        dept.setId("1");
        dept.setName("财务部");
        
        IUser iu = DataAccess.createUser();
        //插入用户
        iu.insert(user);
        //得到ID为1的用户
        User user2 = iu.getUser("1");
        
        IDepartment id = DataAccess.createDepartment();
        id.insert(dept);
        Department dept2 = id.getDepartment("1");
    }
}

输出结果:

在SQL Server 中给User 表增加一条记录
在SQL Server 中根据ID得到User表一条记录
在SQL Server 中给Department表增加一条记录
在SQL Server 中根据ID得到Department表一条记录

如果现在我们增加另一种数据库的访问,相关类的增加是不可避免的,这点无论我们用任何办法都解决不了,不过这叫扩展,开放-封闭原则告诉我们,对于扩展,我们开放。但对于修改,我们应该要尽量避免。就目前而言,我们只需要更改private static final String db="Sqlserver";为private static final String db="Oracle";就能实现数据库之间的切换(当然,类的取名要符合规范:数据库+表,比如oracle数据库中的user表,那么就应该取名为OracleUser)。

如果我们需要增加Project产品类,怎么办?

只需要增加三个与Project相关的类,再修改DataAccess,在其中增加一个public static IProject createProject()方法就可以了。

实际项目通常把数据库的配置放在配置文件中,这里我们仿照一下,用配置文件决定到底使用哪一种数据库:

新增test.properties文件,放在src下,这里设置db=Oracle

修改DataAccess类,在项目加载时根据配置文件中的配置决定使用哪一个数据库

package com.jwen.model3;

import java.util.ResourceBundle;

import com.jwen.model2.IDepartment;
import com.jwen.model2.IUser;

public class DataAccess {
    private static final String packageName="com.jwen.model2";//包名
    private static  String db="Sqlserver";//哪种数据库
    //获取当前线程的类加载器
    private static ClassLoader loader = Thread.currentThread().getContextClassLoader();
    //默认使用SQL Server,但下面的静态代码读取配置文件信息,如果db不为空,则使用db配置的数据库
    static{
        ResourceBundle resource = ResourceBundle.getBundle("test");//test为属性文件名,如果是放在src下,直接用test即可,如果放在其他包中,需添加包路径,如com/jwen/model3/test
        String key = resource.getString("db");  
        if(key!=null&&key.length()>0){
            db = key;
        }
    }
    public static IUser createUser() throws Exception{
        //通过类加载器加载对象
        Class clazz = loader.loadClass(packageName+"."+db+"User");
        //获取类的默认构造器对象并通过它实例化类
        IUser iuser = (IUser) clazz.getDeclaredConstructor(null).newInstance();
        return iuser;
    }
    
    public static IDepartment createDepartment() throws Exception{
        //通过类加载器加载对象
        Class clazz = loader.loadClass(packageName+"."+db+"Department");
        //获取类的默认构造器对象并通过它实例化类
        IDepartment dept = (IDepartment) clazz.getDeclaredConstructor(null).newInstance();
        return dept;
    }
}

测试方法不变,运行结果如下:

在Oracle 中给User 表增加一条记录
在Oracle 中根据ID得到User表一条记录
在Oracle 中给Department表增加一条记录
在Oracle 中根据ID得到Department表一条记录

市面上很多流行框架都使用了抽象工厂模式,我们使用时特别方便,按部就班地配置就行,但了解其内涵可以帮助我们知其然并知其所以然。

抽象工厂模式介绍:http://www.runoob.com/design-pattern/abstract-factory-pattern.html

本文中工程结构如下:

原文地址:https://www.cnblogs.com/jwen1994/p/10132554.html