🚏 导论

抽象工厂模式(Abstract Factory Pattern),提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。当有多个相同的类要实现类似的功能,但是具体的实现细节有所不同,这时候就可以使用抽象工厂模式。


🧀 前置知识


🚦 结构

classDiagram
    class AbstractFactory{
        <<interface>>
        +createProductA()
        +createProductB()
    }
    class ConcreteFactory1{
        +createProductA()
        +createProductB()
    }
    class ConcreteFactory2{
        +createProductA()
        +createProductB()
    }
    class AbstractProductA{
        <<interface>>
    }
    class ProductA1
    class ProductA2
    class AbstractProductB{
        <<interface>>
    }
    class ProductB1
    class ProductB2
    class Client
    AbstractFactory <|.. ConcreteFactory1
    AbstractFactory <|.. ConcreteFactory2
    AbstractProductA <|.. ProductA1
    AbstractProductA <|.. ProductA2
    AbstractProductB <|.. ProductB1
    AbstractProductB <|.. ProductB2
    Client --> AbstractFactory
    Client --> AbstractProductA
    Client --> AbstractProductB

AbstractProductA和AbstractProductB是两个抽象产品,之所以抽象,是因为它们有可能有不同的实现,而ProductA1、ProductA2、ProductB1、ProductB2是对两个抽象产品的具体分类实现。

AbstractFactory是一个抽象工厂,它里面包含所有产品创建的抽象方法,而ConcreteFactory1和ConcreteFactory2就是两个具体的工厂了。

通常在运行时在创建一个ConcreteFactory的实例,这个具体的工厂再创建具有特定实现的产品对象,也就是说,为创建不同的产品对象,客户端应使用不同的具体工厂。


🎭 优缺点分析

😊 优点

  • 易于交换产品系列,由于具体工厂类在一个应用中只需要初始化的时候出现一次,这就使得改编一个应用的具体工厂变得非常简单。它只需要改变具体工厂即可使用不同的产品配置。
  • 它让具体的创建实例过程与客户端分离,客户端通过抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户端代码中。

🙁 缺点

  • 增加需求时,需要增加新的产品类,并且要调整工厂类,这增加了系统的复杂度。
  • 每次使用都需要声明Factory,但是如果要更改产品系列,那么就需要更改所有的Factory声明。

🎬 场景

公司有一个给第三方企业做的电子商务网站,使用SQL Server数据库,已经都大致完成了。但是公司又接了另外一家公司类似的需求项目,但是这家公司想省钱,要用Access数据库。因此任务就变成了要将整个项目调整为用Access数据库。

但是替换的过程中出现了很多问题。

  • SQL Server的命名空间和Access数据库不同:SQL Server上用的是System.Data.SqlClient,而Access上用的是System.Data.OleDb。
  • 数据库操作语法不同:
    • Access插入数据必须用INSERT INTO,而SQL Server用INSERT(不能用INTO)。
    • SQL Server中的GetDate()函数在Access中是Now()。
    • SQL Server中有Substring()函数,而Access中是Mid()。
  • 关键字不同
    • Access不能用password作为字段名,因为它是关键字,而SQL Server可以。要使用关键字需要用[]括起来。

如果今后要换成其他数据库,那么这个项目就要重新调整。这样的设计显然是不合理的。


🛠 解决

最初的代码

用户实体: User.java

public class User {
    private Integer id;
    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

SqlServerUser.java: 用于操作User表

public class SqlServerUser {
    public void insertUser(User user) {
        System.out.println("在SQL Server中给User表增加一条记录");
    }

    public User getUser(Integer id) {
        System.out.println("在SQL Server中根据ID得到User表一条记录");
        return null;
    }
}

Client.java: 客户端测试类

public class Client {
    public static void main(String[] args) {
        User user = new User();
        SqlServerUser su = new SqlServerUser();
        su.insertUser(user);
        su.getUser(1);
        System.out.println("-----------------------------------------");
    }
}

这里之所以不能换数据库,愿意在于SqlServerUser su = new SqlServerUser()使得su这个对象被框死在SQL Server上了。如果这里改成多态,那么在执行‘su.insertUser(user);’和‘su.getUser(1)’时就不需要考虑是Access还是Sql Server了。

初步改造

考虑用工厂方法模式(工厂方法模式是定义一个用于创建对象的接口,让子类决定实例化哪一个类)进行改造。

classDiagram
    class DaoFactory{
        <<interface>>
        +createUserDao()
    }
    class SqlServerDaoFactory{
        +createUserDao()
    }
    class AccessDaoFactory{
        +createUserDao()
    }
    class UserDao {
        <<interface>>
        +insertUser(User user)
        +getUser(Integer id)
    }
    class SqlServerUserDao {
        +insertUser(User user)
        +getUser(Integer id)
    }
    class AccessUserDao {
        +insertUser(User user)
        +getUser(Integer id)
    }
    DaoFactory <|.. SqlServerDaoFactory
    DaoFactory <|.. AccessDaoFactory
    UserDao <|.. SqlServerUserDao
    UserDao <|.. AccessUserDao

UserDao.java: UserDao接口

public interface UserDao {
    /**
     * 插入用户
     * @param user 用户
     */
    void insert(User user);

    /**
     * 获取用户
     * @param id 用户id
     * @return 用户
     */
    User getUser(int id);
}

SqlServerUserDao.java: 用于访问SQL Server的User

public class SqlServerUserDao implements UserDao {
    /**
     * 插入用户
     *
     * @param user 用户
     */
    @Override
    public void insert(User user) {
        System.out.println("在SQL Server中给User表增加一条记录");
    }

    /**
     * 获取用户
     *
     * @param id 用户id
     * @return 用户
     */
    @Override
    public User getUser(int id) {
        System.out.println("在SQL Server中根据id得到User表一条记录");
        return null;
    }
}

AccessUserDao.java: 用于访问Access的User

public class AccessUserDao implements UserDao {
    /**
     * 插入用户
     *
     * @param user 用户
     */
    @Override
    public void insert(User user) {
        System.out.println("在Access中给User表增加一条记录");
    }

    /**
     * 获取用户
     *
     * @param id 用户id
     * @return 用户
     */
    @Override
    public User getUser(int id) {
        System.out.println("在Access中根据id得到User表一条记录");
        return null;
    }
}

DaoFactory.java: 定义一个创建访问User表对象的抽象的工厂接口

public interface DaoFactory {
    /**
     * 创建UserDao对象
     * @return UserDao对象
     */
    UserDao createUserDao();
}

SqlServerDaoFactory.java: 实现SqlFactory接口,实例化SqlServerFactory类

public class SqlServerDaoFactory implements DaoFactory {
    /**
     * 创建UserDao对象
     *
     * @return UserDao对象
     */
    @Override
    public UserDao createUserDao() {
        return new SqlServerUserDao();
    }
}

AccessDaoFactory.java: 实现SqlFactory接口,实例化AccessFactory类

public class AccessDaoFactory implements DaoFactory{
    /**
     * 创建UserDao对象
     *
     * @return UserDao对象
     */
    @Override
    public UserDao createUserDao() {
        return new AccessUserDao();
    }
}

Client.java: 客户端测试类

public class Client {
    public static void main(String[] args) {
        User user = new User();
//        DaoFactory factory = new SqlServerDaoFactory();
        DaoFactory factory = new AccessDaoFactory();
        UserDao userDao = factory.createUserDao();
        userDao.insert(user);
        userDao.getUser(1);
    }
}

这样,如果要换数据库,只需要修改DaoFactory factory = new AccessDaoFactory();即可,而对于UserDao接口的对象userDao事先根本不需要知道是访问哪个数据库。这就是所谓的业务逻辑与数据访问逻辑的解耦。

接下来除了User表,还有Department表,架构就会变成这样:

classDiagram
    direction LR
    class DaoFactory{
        <<interface>>
        +createUserDao()
    }
    class SqlServerDaoFactory{
        +createUserDao()
    }
    class AccessDaoFactory{
        +createUserDao()
    }
    class UserDao {
        <<interface>>
        +insertUser(User user)
        +getUser(Integer id)
    }
    class SqlServerUserDao {
        +insertUser(User user)
        +getUser(Integer id)
    }
    class AccessUserDao {
        +insertUser(User user)
        +getUser(Integer id)
    }
    class DepartmentDao {
        <<interface>>
        +insertDepartment(Department department)
        +getDepartment(Integer id)
    }
    class SqlServerDepartmentDao {
        +insertDepartment(Department department)
        +getDepartment(Integer id)
    }
    class AccessDepartmentDao {
        +insertDepartment(Department department)
        +getDepartment(Integer id)
    }
    DaoFactory <|.. SqlServerDaoFactory
    DaoFactory <|.. AccessDaoFactory
    UserDao <|.. SqlServerUserDao
    UserDao <|.. AccessUserDao
    DepartmentDao <|.. SqlServerDepartmentDao
    DepartmentDao <|.. AccessDepartmentDao

增加DepartmentDao.java,用于客户端访问,解除与具体数据库访问的耦合

public interface DepartmentDao {

    /**
     * 插入部门
     * @param department 部门
     */
    void insert(Department department);

    /**
     * 根据id查询部门
     * @param id id
     * @return 部门
     */
    Department getDepartment(Integer id);
}

SqlServerDepartmentDao.java: 用于访问SQL Server的Department

public class SqlServerDepartmentDao implements DepartmentDao {
    /**
     * 插入部门
     *
     * @param department 部门
     */
    @Override
    public void insert(Department department) {
        System.out.println("在SQL Server中给Department表增加一条记录");
    }

    /**
     * 根据id查询部门
     *
     * @param id id
     * @return 部门
     */
    @Override
    public Department getDepartment(Integer id) {
        System.out.println("在SQL Server中根据id得到Department表一条记录");
        return null;
    }
}

AccessDepartmentDao.java: 用于访问Access的Department

public class AccessDepartmentDao implements DepartmentDao {
    /**
     * 插入部门
     *
     * @param department 部门
     */
    @Override
    public void insert(Department department) {
        System.out.println("在Access中给Department表增加一条记录");
    }

    /**
     * 根据id查询部门
     *
     * @param id id
     * @return 部门
     */
    @Override
    public Department getDepartment(Integer id) {
        System.out.println("在Access中根据id得到Department表一条记录");
        return null;
    }
}

在DaoFactory.java中增加创建DepartmentDao对象的方法

    /**
     * 创建DepartmentDao对象
     * @return DepartmentDao对象
     */
    DepartmentDao createDepartmentDao();

SqlServerDaoFactory.java中增加创建DepartmentDao对象的方法

    /**
     * 创建DepartmentDao对象
     *
     * @return DepartmentDao对象
     */
    @Override
    public DepartmentDao createDepartmentDao() {
        return new SqlServerDepartmentDao();
    }

AccessDaoFactory.java中增加创建DepartmentDao对象的方法

    /**
     * 创建DepartmentDao对象
     *
     * @return DepartmentDao对象
     */
    @Override
    public DepartmentDao createDepartmentDao() {
        return new AccessDepartmentDao();
    }

最后Client.java

public class Client {
    public static void main(String[] args) {
        User user = new User();
        DaoFactory factory = new SqlServerDaoFactory();
//        DaoFactory factory = new AccessDaoFactory();
        UserDao userDao = factory.createUserDao();
        userDao.insert(user);
        userDao.getUser(1);

        DepartmentDao departmentDao = factory.createDepartmentDao();
        departmentDao.insert(new Department());
        departmentDao.getDepartment(1);
    }
}

输出的结果为:

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

这样整个架构已经演进为了抽象工厂模式。当只有一个类需要被工厂方法创建的时候,是工厂方法模式,而涉及多个产品系列的问题,这种工厂模式叫做抽象工厂模式(Abstract Factory Pattern)。

用简单工厂模式改进抽象工厂

由于抽象工厂的缺点中提到,每次使用都需要声明Factory,但是如果要更改产品系列,那么就需要更改所有的Factory声明。这里可以用简单工厂模式改进。

直接去除DaoFactory、SqlServerDaoFactory、AccessDaoFactory,改用DataAccess类。

classDiagram
    class UserDao{
      <<interface>>
    }
    class DepartmentDao{
      <<interface>>
    }
    class SqlServerUserDao
    class SqlServerDepartmentDao
    class AccessUserDao
    class AccessDepartmentDao
    
    class DataAccess{
      -db : string
      +createUser() UserDao
      +createDepartment() DepartmentDao
    }

    UserDao <|.. SqlServerUserDao
    UserDao <|.. AccessUserDao
    DepartmentDao <|.. SqlServerDepartmentDao
    DepartmentDao <|.. AccessDepartmentDao
    DataAccess ..> UserDao
    DataAccess ..> DepartmentDao

DataAccess.java

public class DataAccess {
    private static final String DB = "SQLServer";
//    private static final String DB = "Access";

    /**
     * 创建用户Dao
     * @return UserDao
     */
    public static UserDao createUserDao(){
        UserDao userDao = null;
        switch (DB){
            case "SQLServer":
                userDao = new SqlServerUserDao();
                break;
            case "Access":
                userDao = new AccessUserDao();
                break;
            default:
                break;
        }
        return userDao;
    }

    /**
     * 创建部门Dao
     * @return DepartmentDao
     */
    public static DepartmentDao createDepartmentDao(){
        DepartmentDao departmentDao = null;
        switch (DB){
            case "SQLServer":
                departmentDao = new SqlServerDepartmentDao();
                break;
            case "Access":
                departmentDao = new AccessDepartmentDao();
                break;
            default:
                break;
        }
        return departmentDao;
    }
}

Client.java 客户端代码

public class Client {
    public static void main(String[] args) {
        User user = new User();
        Department department = new Department();

        UserDao userDao = DataAccess.createUserDao();
        userDao.insert(user);
        userDao.getUser(1);

        DepartmentDao departmentDao = DataAccess.createDepartmentDao();
        departmentDao.insert(department);
        departmentDao.getDepartment(1);
    }
}

这样的设计客户端代码可以不出现具体数据库名称,使得需要修改数据库时,只需要在DataAccess修改DB的内容就可以了。但是如果需要增加新的数据库支持,就还需要调整Access的switch结构,原来只需要增加一个工厂类就好了。

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

为了解决既不修改switch代码结构,也不让客户端出现具体数据库名称。可以引入反射的思想,通过字符串实例化对应的类。 修改后如下所示

public class DataAccess {
    private static final String DB = "SqlServer";
    private static final String packageName = "space.rexhub.designpatterns.creational.abstract_factory.dao.impl";

    /**
     * 创建用户Dao
     * @return UserDao
     */
    public static UserDao createUserDao(){
        UserDao userDao = null;
        String className = packageName + "." + DB + "UserDao";
        try {
            Constructor<?> constructor = Class.forName(className).getDeclaredConstructor();
            userDao = (UserDao) constructor.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return userDao;
    }

    /**
     * 创建部门Dao
     * @return DepartmentDao
     */
    public static DepartmentDao createDepartmentDao(){
        DepartmentDao departmentDao = null;
        String className = "space.rexhub.designpatterns.creational.abstract_factory.dao.impl." + DB + "DepartmentDao";
        try {
            Constructor<?> constructor = Class.forName(className).getConstructor();
            departmentDao = (DepartmentDao) constructor.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return departmentDao;
    }
}

这样,只需要修改DB的值,就可以实现不同数据库的切换,并且不需要修改switch结构。如果我们需要提供Oracle的支持,只需要创建OracleUserDao和OracleDepartmentDao类,然后修改DB的值即可。

但还是可以挑出毛病,切换数据库还是需要在代码里进行修改,能否不修改代码就能切换数据库呢?

用反射+配置文件实现数据访问程序

为了不修改数据库就能切换数据库,可以将数据库名称放在配置文件中,然后通过反射实例化对应的类。

创建一个 database_config.properties文件,内容如下:

DB = SqlServer

DataAccess.java只需要从配置文件中获取DB就可以了

public class DataAccess {
    private static final String PACKAGE_NAME = "space.rexhub.design_patterns.creational.abstract_factory.dao.impl";
    private static String DB;

    static{
        Properties properties = new Properties();
        try {
            properties.load(new FileInputStream("space/rexhub/design_patterns/creational/abstract_factory/database_config.properties"));
            DB = (String) properties.get("DB");
        } catch (IOException e) {
            DB = null;
        }
    }
    /**
     * 创建用户Dao
     * @return UserDao
     */
    public static UserDao createUserDao(){
        UserDao userDao = null;
        String className = PACKAGE_NAME + "." + DB + "UserDao";
        try {
            Constructor<?> constructor = Class.forName(className).getDeclaredConstructor();
            userDao = (UserDao) constructor.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return userDao;
    }

    /**
     * 创建部门Dao
     * @return DepartmentDao
     */
    public static DepartmentDao createDepartmentDao(){
        DepartmentDao departmentDao = null;
        String className = PACKAGE_NAME + "." + DB + "DepartmentDao";
        try {
            Constructor<?> constructor = Class.forName(className).getConstructor();
            departmentDao = (DepartmentDao) constructor.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return departmentDao;
    }
}

这样这个数据库切换的问题就可以相对较完美的解决了,我们应用了反射+抽象工厂模式解决了数据库访问时的可维护、可扩展问题。