大话设计模式笔记(十二)の抽象工厂模式

举个栗子

问题描述

模拟访问数据库“新增用户”和“得到用户”,用户类假设只有ID和Name两个字段。

简单实现

User

/**
 * 用户类
 * Created by callmeDevil on 2019/7/28.
 */
public class User {

    private int id;
    private String name;

    // 省略 get set 方法

}

SqlServerUser

/**
 * 假设sqlServer 连接,用于操作User表
 * Created by callmeDevil on 2019/7/28.
 */
public class SqlServerUser {

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

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

}

测试

public class Test {

    public static void main(String[] args) {
        User user = new User();
        SqlServerUser su = new SqlServerUser();
        su.insert(user);
        su.getUser(user.getId());
    }

}

测试结果

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

存在问题

如果需要连接别的数据库,那么这个写法无法扩展,下面使用工厂方法模式实现

工厂方法模式实现

IUser

/**
 * 用于客户端访问,解除与具体数据库访问的耦合
 * Created by callmeDevil on 2019/7/28.
 */
public interface IUser {
    void insert(User user);
    User getUser(int id);
}

SqlServerUser

/**
 * 用于访问SQL Server 的User
 * Created by callmeDevil on 2019/7/28.
 */
public class SqlServerUser implements IUser {

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

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

}

AccessUser

/**
 * 用于访问Access 的User
 * Created by callmeDevil on 2019/7/28.
 */
public class AccessUser implements IUser {

    @Override
    public void insert(User user) {
        System.out.println("在Access 中给User表增加一条记录");
    }

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

}

IFactory

/**
 * 定义一个创建访问User 表对象的抽象工厂接口
 * Created by callmeDevil on 2019/7/28.
 */
public interface IFactory {
    IUser createUser();
}

SqlServerFactory

/**
 * 实现IFactory 接口,实例化SQLServerUser
 * Created by callmeDevil on 2019/7/28.
 */
public class SqlServerFactory implements IFactory {
    @Override
    public IUser createUser() {
        return new SqlServerUser();
    }
}

AccessFactory

/**
 * 实现IFactory 接口,实例化AccessUser
 * Created by callmeDevil on 2019/7/28.
 */
public class AccessFactory implements IFactory {
    @Override
    public IUser createUser() {
        return new AccessUser();
    }
}

测试

public class Test {
    public static void main(String[] args) {
        User user = new User();
        // 若要更改成 Access 数据库,只需要将此处改成
        // IFactory factory = new AccessFactory();
        IFactory factory = new SqlServerFactory();
        IUser iUser = factory.createUser();
        iUser.insert(user);
        iUser.getUser(1);
    }
}

测试结果同上。

增加需求

如果要增加一个部门表(Department),需要怎么改?

修改实现

Department

/**
 * 部门表
 * Created by callmeDevil on 2019/7/28.
 */
public class Department {

    private int id;
    private String name;

    // 省略 get set 方法

}

IDepartment

/**
 * 用于客户端访问,解除与具体数据库访问的耦合
 * Created by callmeDevil on 2019/7/28.
 */
public interface IDepartment {
    void insert(Department department);
    Department getDepartment(int id);
}

SqlServerDepartment

/**
 * 用于访问SqlServer 的Department
 * Created by callmeDevil on 2019/7/28.
 */
public class SqlServerDepartment implements IDepartment {

    @Override
    public void insert(Department department) {
        System.out.println("在 SqlServer 中给Department 表增加一条记录");
    }

    @Override
    public Department getDepartment(int id) {
        System.out.println("在SQL Server中根据ID得到Department表一条记录");
        return null;
    }

}

AccessDepartment

/**
 * 用于访问Access 的Department
 * Created by callmeDevil on 2019/7/28.
 */
public class AccessDepartment implements IDepartment {

    @Override
    public void insert(Department department) {
        System.out.println("在Access 中给Department 表增加一条记录");
    }

    @Override
    public Department getDepartment(int id) {
        System.out.println("在Access 中根据ID得到Department表一条记录");
        return null;
    }

}

IFactory

/**
 * 定义一个创建访问User 表对象的抽象工厂接口
 * Created by callmeDevil on 2019/7/28.
 */
public interface IFactory {
    IUser createUser();
    IDepartment createDepartment(); //增加的接口方法
}

SqlServerFactory

/**
 * 实现IFactory 接口,实例化SQLServerUser
 * Created by callmeDevil on 2019/7/28.
 */
public class SqlServerFactory implements IFactory {

    @Override
    public IUser createUser() {
        return new SqlServerUser();
    }

    @Override
    public IDepartment createDepartment() {
        return new SqlServerDepartment(); //增加了SqlServerDepartment 工厂
    }

}

AccessFactory

/**
 * 实现IFactory 接口,实例化AccessUser
 * Created by callmeDevil on 2019/7/28.
 */
public class AccessFactory implements IFactory {

    @Override
    public IUser createUser() {
        return new AccessUser();
    }

    @Override
    public IDepartment createDepartment() {
        return new AccessDepartment(); //增加了AccessDepartment 工厂
    }

}

测试

public class Test {
    public static void main(String[] args) {
        User user = new User();
        Department dept = new Department();
        // 只需确定实例化哪一个数据库访问对象给 factory
        IFactory factory = new AccessFactory();
        // 则此时已于具体的数据库访问解除了依赖
        IUser iUser = factory.createUser();
        iUser.insert(user);
        iUser.getUser(1);

        IDepartment iDept = factory.createDepartment();
        iDept.insert(dept);
        iDept.getDepartment(1);
    }
}

测试结果

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

抽象工厂模式

定义

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

UML图

代码实现

实际上上面的修改实现已经满足抽象工厂模式的实现方式,此处不再举例。

优缺点

优点

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

缺点

如果还要添加对项目表(Project)的访问,那么需要增加三个类,IProject、SQLServerProject、AccessProject,还需要更改 IFactory、ISQLServerFactory、AccessFactory 才可以完全实现,这太糟糕了。编程是门艺术,这样大批量的改动,显然是非常丑陋的做法。

用简单工厂来改进抽象工厂

去除IFactory、SQLServerFactory、AccessFactory,改为一个 DataAccess,用一个简单工厂模式来实现。

结构图

代码实现

DataAccess

/**
 * 统一管理数据库访问
 * Created by callmeDevil on 2019/7/28.
 */
public class DataAccess {

    // 数据库名称,可替换成 Access
    private static final String DB = "SqlServer";
//    private static final String DB = "Access";

    public static IUser createUser() {
        IUser user = null;
        switch (DB) {
            case "SqlServer":
                user = new SqlServerUser();
                break;
            case "Access":
                user = new AccessUser();
                break;
            default:
                break;
        }
        return user;
    }

    public static IDepartment createDepartment() {
        IDepartment department = null;
        switch (DB) {
            case "SqlServer":
                department = new SqlServerDepartment();
                break;
            case "Access":
                department = new AccessDepartment();
                break;
            default:
                break;
        }
        return department;
    }

}

测试

public class Test {
    public static void main(String[] args) {
        User user = new User();
        Department dept = new Department();
        // 直接得到实际的数据库访问实例,而不存在任何的依赖
        IUser iUser = DataAccess.createUser();
        iUser.insert(user);
        iUser.getUser(1);

        IDepartment iDept = DataAccess.createDepartment();
        iDept.insert(dept);
        iDept.getDepartment(1);
    }
}

测试结果

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

存在问题

虽然解决了抽象工厂模式中需要修改太多地方的问题,但又回到了简单工厂模式一开始的问题了,就是如果要连接 Oracle 数据库,那么需要修改的地方则是 DataAccess 类中所有方法的 swicth 中加 case 分支了。

用配置文件+反射+抽象工厂实现

配置文件(db.properties)

# 数据库名称,可更改成 Access
db=SqlServer

DataAccess

/**
 * 统一管理数据库访问
 * Created by callmeDevil on 2019/7/28.
 */
public class DataAccess {

    // 数据库名称,从配置文件中获取
    private static String DB;

    public static IUser createUser() throws Exception {
        if (DB == null || DB.trim() == "") {
            return null;
        }
        // 拼接具体数据库访问类的权限定名
        String className = "com.xxx." + DB + "User";
        return (IUser) Class.forName(className).newInstance();
    }

    public static IDepartment createDeptment() throws Exception {
        if (DB == null || DB.trim() == "") {
            return null;
        }
        // 拼接具体数据库访问类的权限定名
        String className = "com.xxx." + DB + "Department";
        return (IDepartment) Class.forName(className).newInstance();
    }

    public static String getDB() {
        return DB;
    }

    public static void setDB(String DB) {
        DataAccess.DB = DB;
    }

}

测试

public class Test {
    public static void main(String[] args) throws Exception {
        // 加载配置文件
        Properties properties = new Properties();
        InputStream is = new FileInputStream(new File("xxx\\db.properties")); // 配置文件所在路径,当前方式采用绝对路径获取
        properties.load(is);
        is.close();
        String db = properties.getProperty("db");
        // 使用具体的数据库告诉管理类
        DataAccess dataAccess = new DataAccess();
        dataAccess.setDB(db);

        User user = new User();
        IUser iUser = dataAccess.createUser();
        iUser.insert(user);
        iUser.getUser(1);

        Department dept = new Department();
        IDepartment iDept = dataAccess.createDeptment();
        iDept.insert(dept);
        iDept.getDepartment(1);
    }
}

测试结果

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

现在如果我们增加了 Oracle 数据库访问,相关类的增加是不可避免的,这点无论用任何办法都解决不了,不过这叫扩展,开放-封闭原则告诉我们,对于扩展,我们开放,但对于修改,我们应该尽量关闭,就目前实现方式而言,只需要将配置文件中改为 Oracle (如果新增的具体访问类名称为 OracleUserOracleDepartment 的话)即可达到目的,客户端也不需要任何修改。

反射的好处

所有在用简单工厂的地方,都可以考虑用反射技术来去除 switch 或 if,解除分支判断带来的耦合。

总结

可以发现到目前为止,就“工厂”而言,已经包含了三种设计模式:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

对上述模式的不同点将在后续推出,本文篇幅已经过长,此处先不叙述。

posted @ 2019-07-28 17:13  callmeDevil  阅读(511)  评论(0编辑  收藏  举报