4.JAVA抽象工厂模式总结

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

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实现类,然后再修改配置文件,这样就对源码无需任何修改。也算是把抽象工厂模式发挥到极致了~~~~~~

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容