美文网首页
4.JAVA抽象工厂模式总结

4.JAVA抽象工厂模式总结

作者: ironman327 | 来源:发表于2018-09-22 14:33 被阅读0次

    这篇我们将介绍最后一种工厂模式,抽象工厂模式

    0.抛出问题

    很多博客和书都喜欢用很生活化的例子来实现设计模式的代码,例如造车、做奶茶什么的,不过笔者反而认为那会使得难以理解,难以和实际开发联系起来,所以我更喜欢举一些实际开发中的例子

    业务需求:
    1.使用MySQL数据库设计DAO层对user表的查询和添加
    2.在未来或许会更换数据库
    3.在未来或许会添加表

    首先用工厂方法模式

    //一个User表的DAO接口
    public interface UserDAO {
        User queryById(String id);
        void insert(User user);
    }
    
    //基于MySQL的实现类
    public class MySQLUserDAOImpl implements UserDAO {
        @Override
        public User queryById(String id) {
            System.out.println("在MySQL中查询到一条user数据");
            return null;
        }
    
        @Override
        public void insert(User user) {
            System.out.println("在MySQL中插入一条user数据");
        }
    }
    
    //创建各种DAO对象的工厂接口
    public interface DAOFactory {
        UserDAO createUserDAO();
    }
    
    //创建基于实现的MySQL的DAO的工厂
    public class MySQLFactory implements DAOFactory {
        @Override
        public UserDAO createUserDAO() {
            return new MySQLUserDAOImpl();
        }
    }
    
    //测试代码
    public class Main {
        public static void main(String[] args)
        {
            DAOFactory daoFactory = new MySQLFactory();
            UserDAO userDAO = daoFactory.createUserDAO();
            userDAO.insert(new User());
            userDAO.queryById("");
        }
    }
    
    //运行结果
    在MySQL中插入一条user数据
    在MySQL中查询到一条user数据
    
    Process finished with exit code 0
    

    熟悉工厂方法模式的朋友们很快就可以写出上面的代码,我们现在来看第二个业务需求,如果需要更换数据库。

    假设我们更换为Oracle

    //UserDAO基于Oracle的实现类
    public class OracleUserDAOImpl implements UserDAO {
        @Override
        public User queryById(String id) {
            System.out.println("在Oracle中查询到一条user数据");
            return null;
        }
    
        @Override
        public void insert(User user) {
            System.out.println("在Oracle中添加一条user数据");
        }
    }
    
    //创建基于实现的 Oracle的DAO的工厂
    public class OracleFactory implements DAOFactory {
        @Override
        public UserDAO createUserDAO() {
            return new OracleUserDAOImpl();
        }
    }
    
    public class Main {
        public static void main(String[] args)
        {
            //修改客户端代码
            DAOFactory daoFactory = new OracleFactory();
            UserDAO userDAO = daoFactory.createUserDAO();
            userDAO.insert(new User());
            userDAO.queryById("");
        }
    }
    
    //运行结果
    在Oracle中添加一条user数据
    在Oracle中查询到一条user数据
    
    Process finished with exit code 0
    

    似乎一切依旧很顺利,不过在添加Oracle的代码后,我们需要修改主函数里的代码,要是只有一两处还好办,不过DAO层的操作在工程中通常是很多的,也就是说可能在代码中有很多地方都用到了最初的 DAOFactory daoFactory = MysqlFactory(); 如果直接更改为 DAOFactory daoFactory = new OracleFactory(); 那所牵涉的修改一定不止一处。这个地方有bad small,需要斟酌。

    上面的问题先放着,我们继续看第三个需求,如果需要添加表,例如部门表(dept)

    //一个Dept表的DAO接口
    public interface DeptDAO {
        Dept queryById(String id);
        void insert(Dept user);
    }
    
    //基于MySQL的实现类
    public class MySQLDeptDAOImpl implements DeptDAO {
        @Override
        public Dept queryById(String id) {
            System.out.println("在MySQL中查询到一条Dept数据");
            return null;
        }
    
        @Override
        public void insert(Dept user) {
            System.out.println("在MySQL中插入一条Dept数据");
        }
    }
    
    //基于Oracle的实现类
    public class OracleDeptDAOImpl implements DeptDAO {
        @Override
        public Dept queryById(String id) {
            System.out.println("在Oracle中查询到一条Dept数据");
            return null;
        }
    
        @Override
        public void insert(Dept user) {
            System.out.println("在Oracle中插入一条Dept数据");
        }
    }
    
    public interface DAOFactory {
        UserDAO createUserDAO();
        DeptDAO createDeptDAO();//添加接口方法
    }
    
    public class MySQLFactory implements DAOFactory {
        @Override
        public UserDAO createUserDAO() {
            return new MySQLUserDAOImpl();
        }
        //增加创建MySQLdept的工厂方法
        @Override
        public DeptDAO createDeptDAO() {
            return new MySQLDeptDAOImpl();
        }
    }
    
    public class OracleFactory implements DAOFactory {
        @Override
        public UserDAO createUserDAO() {
            return new OracleUserDAOImpl();
        }
        //增加创建Oracledept的工厂方法
        @Override
        public DeptDAO createDeptDAO() {
            return new OracleDeptDAOImpl();
        }
    }
    
    //修改客户端
    public class Main {
        public static void main(String[] args)
        {
            DAOFactory daoFactory = new OracleFactory();
            UserDAO userDAO = daoFactory.createUserDAO();
            userDAO.insert(new User());
            userDAO.queryById("");
            DeptDAO deptDAO = daoFactory.createDeptDAO();
            deptDAO.insert(new Dept());
            deptDAO.queryById("");
        }
    }
    
    \\测试结果
    在Oracle中添加一条user数据
    在Oracle中查询到一条user数据
    在Oracle中插入一条Dept数据
    在Oracle中查询到一条Dept数据
    
    Process finished with exit code 0
    

    其实在上面一步一步的改动中,已经渐渐出现了抽象设计模式的概念。只有一个User类和User操作类的时候,是只需要工厂方法模式的,但现在数据库有多张表,而MySQL和Oracle又是两大分类,这种有多个维度来影响具体产品的问题,我们就使用抽象模式。

    1.抽象工厂模式


    (图有点丑,大家将就看一下= =)我觉得比UML图要好理解一丢丢。我们来思考一下这样设计的优点是什么,上面的程序中,有MySQL和Oracle两种数据库,在项目中通常我们只会使用其中一种。当一个产品族(数据库)中的多个对象(多个表)被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。如上图最后的主函数代码,如果更换数据库为MySQL,只需要把DAOFactory daoFactory = new OracleFactory()换成DAOFactory daoFactory = new MySQLFactory(),而后面的UserDAO类和DeptDAO类并不需要改变,他们也无需关心背后是由MySQL还是Oracle实现。

    其实这种思想在软件设计中随处可见,如手机QQ中的个性主题,当我们更换主题后,很多图标、聊天框等样式都会随之改变。这就是在换掉产品工厂(主题)以后,所有的产品组中的对象(各种图标)直接就被更换了。再举一个生活中的例子,日本在更换首相以后,通常都会组建新内阁,把首相比作工厂的话,那内阁成员都是产品组里的成员。

    不过该模式有什么缺点呢?其实在上面的代码已经体现得很明显了,当我们加入Dept表的时候,除了增加两个类,我们还修改了三处源码,DAOFactory、 OracleFactory、MySQLFactory,这是极其不符合开放封闭原则的。

    2.用反射+配置文件+单例设计模式来改进抽象工厂

    谈谈为什么需要单例设计模式。在单例模式的文章中我们提到,如果功能重复且不变的情况下,我们可以使用一个对象而非重复的new对象,在web开发的service层,我们往往会用到dao层的方法,而一张表仅仅对应一个dao对象的,所以只需要一个dao对象对相应的表进行操作即可。
    //代码如下

    public class MySQLDeptDAOImpl implements DeptDAO {
        volatile private static MySQLDeptDAOImpl mySQLDeptDAO = null;
        private MySQLDeptDAOImpl(){
    
        }
        public static MySQLDeptDAOImpl getMySQLDeptDAO(){
            if (mySQLDeptDAO == null)
                synchronized (MySQLDeptDAOImpl.class) {
                    if (mySQLDeptDAO == null)
                        mySQLDeptDAO = new MySQLDeptDAOImpl();
                }
            return mySQLDeptDAO;
        }
        @Override
        public Dept queryById(String id) {
            System.out.println("在MySQL中查询到一条Dept数据");
            return null;
        }
    
        @Override
        public void insert(Dept user) {
            System.out.println("在MySQL中插入一条Dept数据");
        }
    }
    
    public class MySQLUserDAOImpl implements UserDAO {
        volatile private static MySQLUserDAOImpl mySQLUserDAO = null;
        private MySQLUserDAOImpl(){
    
        }
        public static MySQLUserDAOImpl getMySQLUserDAO(){
            if (mySQLUserDAO == null)
                synchronized (MySQLUserDAOImpl.class) {
                    if (mySQLUserDAO == null)
                        mySQLUserDAO = new MySQLUserDAOImpl();
                }
            return mySQLUserDAO;
        }
        @Override
        public User queryById(String id) {
            System.out.println("在MySQL中查询到一条user数据");
            return null;
        }
    
        @Override
        public void insert(User user) {
            System.out.println("在MySQL中插入一条user数据");
        }
    }
    

    由于篇幅,笔者只更换了MySQL的实现类。

    在上面我们提到了如果需要更换数据库,其实依然需要指明具体的工厂,如果我们能把这种不确定放在运行时而不是编译时,通过外部配置文件决定数据库的类别,就可以大大的降低耦合性,使得在更换数据库的时候十分简单。

    现在开始动手,先写一个配置文件config.properties

    databaseName=MySQL
    

    上面的DAOFactory以及它的实现类我们都可以删掉了。上新的DAOFactory代码

    public class DAOFactory {
        private static String database;
        static {
            try {
                Properties properties = new Properties();
                InputStream inputStream = DAOFactory.class.getClassLoader().getResourceAsStream("config.properties");
                properties.load(inputStream);
                database = properties.getProperty("databaseName");//根据配置文件确定数据库类别
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        public static UserDAO createUser() {
            String className = "com.ironman.dao." + database + "UserDAOImpl";
            try {
                Class<UserDAO> userDAOClass = (Class<UserDAO>) Class.forName(className);//根据数据库类别决定使用哪种产品类
                Method newInstanceMethod = userDAOClass.getMethod("getMySQLUserDAO");//返回得到实例对象的方法对象
                return (UserDAO)newInstanceMethod.invoke(userDAOClass);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static DeptDAO createDept(){
            String className = "com.ironman.dao." + database + "DeptDAOImpl";
            try {
                Class<DeptDAO> deptDAOClass = (Class<DeptDAO>) Class.forName(className);//根据数据库类别决定使用哪种产品类
                Method newInstanceMethod = deptDAOClass.getMethod("getMySQLDeptDAO");//返回得到实例对象的方法对象
                return (DeptDAO) newInstanceMethod.invoke(deptDAOClass);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    
    public class Main {
        public static void main(String[] args)
        {
            UserDAO userDAO = DAOFactory.createUser();
            userDAO.queryById("");
            userDAO.insert(new User());
            DeptDAO deptDAO = DAOFactory.createDept();
            deptDAO.queryById("");
            deptDAO.insert(new Dept());
        }
    }
    
    //运行结果
    在MySQL中查询到一条user数据
    在MySQL中插入一条user数据
    在MySQL中查询到一条Dept数据
    在MySQL中插入一条Dept数据
    
    Process finished with exit code 0
    

    这样设计,当我们更换数据库的时候,如现在需要更换SQL Server,我们只需要根据命名规范写出DeptDAO和UserDAO的SQL Server实现类,然后再修改配置文件,这样就对源码无需任何修改。也算是把抽象工厂模式发挥到极致了~~~~~~

    前面的简单工厂也可以用反射改造,这里就不举例说明了。将编译时依赖转为运行时依赖真的是一个很重要的思想,有一种展望未来的感觉~希望大家在写代码的时候也可以多加思考,写出优雅的代码

    相关文章

      网友评论

          本文标题:4.JAVA抽象工厂模式总结

          本文链接:https://www.haomeiwen.com/subject/booznftx.html