这篇我们将介绍最后一种工厂模式,抽象工厂模式
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实现类,然后再修改配置文件,这样就对源码无需任何修改。也算是把抽象工厂模式发挥到极致了~~~~~~
前面的简单工厂也可以用反射改造,这里就不举例说明了。将编译时依赖转为运行时依赖真的是一个很重要的思想,有一种展望未来的感觉~希望大家在写代码的时候也可以多加思考,写出优雅的代码
网友评论