初步认知:抽象工厂模式
Posted jwen1994
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初步认知:抽象工厂模式相关的知识,希望对你有一定的参考价值。
在讲述这个模式之前,我们先看一个案例:模拟最基本的数据库访问:获取用户,向用户表插入记录
//用户类,假设只有ID和Name两个字段 public class User { private String id; private String name; //省略getter、setter方法 } //SqlserverUser类,用于操作User表,假设只有“新增用户”、“得到用户”,其余方法以及具体的SQL语句省略 public class SqlserverUser { public void insert(User user){ System.out.println("在SQL Server 中给User 表增加一条记录"); } public User getUser(String id){ System.out.println("在SQL Server 中根据ID得到User表一条记录"); return null; } } //测试方法 public class Test { public static void main(String[] args) { User user = new User(); user.setId("1"); user.setName("张三"); SqlserverUser su = new SqlserverUser(); //插入用户 su.insert(user); //得到ID为1的用户 User user2 = su.getUser("1"); } }
这里的SqlserverUser su = new SqlserverUser()使得su这个对象被框死在SQL Server上了,如果这里是灵活的,专业点的说法,是多态的,那么在执行su.insert(user)和su.getUser("1")的时候,就不用在意是哪一种数据库了。
现在我们用“工厂方法模式”来封装new SqlserverUser()所造成的变化。工厂方法模式是定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法模式的讲解请参考另一边文章:https://www.cnblogs.com/jwen1994/p/9970048.html
//IUser接口,解除与具体数据库访问的耦合 public interface IUser { void insert(User user); User getUser(String id); } //SqlserverUser类,用于访问SQL Server的User public class SqlserverUser implements IUser{ public void insert(User user){ System.out.println("在SQL Server 中给User 表增加一条记录"); } public User getUser(String id){ System.out.println("在SQL Server 中根据ID得到User表一条记录"); return null; } } //OracleUser类,用于访问Oracle的User public class OracleUser implements IUser { public void insert(User user){ System.out.println("在Oracle 中给User 表增加一条记录"); } public User getUser(String id){ System.out.println("在Oracle 中根据ID得到User表一条记录"); return null; } }
IUser接口,解除与具体数据库访问的耦合。那么使用哪个数据库又该怎么确定呢?这里创建 IUser的工厂类,用来创建具体的数据库实现类。
//IFactory接口,定义一个创建访问User表对象的抽象的工厂接口 public interface IFactory { IUser createUser(); } //SqlServerFactory类,实现IFactory接口,实例化SqlserverUser public class SqlServerFactory implements IFactory { public IUser createUser() { return new SqlserverUser(); } } //OracleFactory 类,实现IFactory接口,实例化OracleUser public class OracleFactory implements IFactory{ public IUser createUser() { return new OracleUser(); } }
测试方法
public class Test { public static void main(String[] args) { User user = new User(); user.setId("1"); user.setName("张三"); //若要更改成Oracle数据库,只需要将本句改成IFactory factory = new OracleFactory(); IFactory factory = new SqlServerFactory(); IUser iu = factory.createUser(); //插入用户 iu.insert(user); //得到ID为1的用户 User user2 = iu.getUser("1"); } }
现在如果要换数据库,只需要把new SqlServerFactory()改成new OracleFactory(),由于多态的关系,使得声明IUser接口的对象iu事先根本不知道是在访问哪个数据库,却可以在运行时很好地完成工作,这就是所谓的业务逻辑与数据访问的解耦。
现在还有两个问题:1、代码里需要声明new SqlServerFactory(),这里只是一处声明,如果有很多处,岂不是都需要更改? 2、数据库里不可能只有一张表,如果需要增加一张部门表(Department表),怎么办?
我们先处理第二个问题。
//部门类 public class Department { private String id; private String name; //省略getter、setter方法 } //IDepartment接口,解除与具体数据库访问的耦合 public interface IDepartment { void insert(Department department); Department getDepartment(String id); } //SqlserverDepartment用于访问SQL Server的Department public class SqlserverDepartment implements IDepartment{ public void insert(Department department){ System.out.println("在SQL Server 中给Department表增加一条记录"); } public Department getDepartment(String id){ System.out.println("在SQL Server 中根据ID得到Department表一条记录"); return null; } } //OracleDepartment 用于访问Oracle的Department public class OracleDepartment implements IDepartment{ public void insert(Department department){ System.out.println("在Oracle 中给Department表增加一条记录"); } public Department getDepartment(String id){ System.out.println("在Oracle 中根据ID得到Department表一条记录"); return null; } }
public interface IFactory { IUser createUser(); //增加接口方法 IDepartment createDepartment(); } public class SqlServerFactory implements IFactory { public IUser createUser() { return new SqlserverUser(); } //增加SqlserverDepartment工厂 public IDepartment createDepartment() { return new SqlserverDepartment(); } } public class OracleFactory implements IFactory{ public IUser createUser() { return new OracleUser(); } //增加OracleDepartment工厂 public IDepartment createDepartment() { return new OracleDepartment(); } }
测试方法
public class Test { public static void main(String[] args) { User user = new User(); user.setId("1"); user.setName("张三"); Department dept = new Department(); dept.setId("1"); dept.setName("财务部"); //只需确定实例化哪一个数据库访问对象给factory //IFactory factory = new SqlServerFactory(); IFactory factory = new OracleFactory(); //此时已与具体的数据库访问解除了依赖 IUser iu = factory.createUser(); //插入用户 iu.insert(user); //得到ID为1的用户 User user2 = iu.getUser("1"); //此时已与具体的数据库访问解除了依赖 IDepartment id = factory.createDepartment(); id.insert(dept); Department dept2 = id.getDepartment("1"); } }
输出结果:
在Oracle 中给User 表增加一条记录
在Oracle 中根据ID得到User表一条记录
在Oracle 中给Department表增加一条记录
在Oracle 中根据ID得到Department表一条记录
看到这里,有的人会有疑问:这个例子中,哪些部分和抽象工厂模式有关呢?之前说的第一个问题还没解决呢!!!
如果你自己看了上面介绍工厂方法模式的文章,对比了两个例子,你会发现本文的例子中工厂接口IFactory定义了两个方法。难道这就是区别?
对,这就是区别!
抽象方法的定义:提供一个创建一系列相关或互相依赖对象的接口,而无需指定他们具体的类。
一系列相关或互相依赖对象:本例中它们都依赖数据库
无需指定他们具体的类:具体的实现类在子工厂中才会被创建出来。
这样做的好处是什么呢?
最大的好处便是易于交换产品系列,由于具体工厂类,例如IFactoiy factory = new OracleFactory(), 在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。我们的设计不能去防止需求的更改,那么我们的理想便是让改动变得最小,现在如果你要更改数据库访问,我们只需要更改具体工厂就可以做到。第二大好处是,它让具体的创建实例过程与main方法分离,main方法是通过它们的抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在main方法代码中。事实上,上面的例子,main方法所认识的只有lUser和IDepartment,至于它是用SQL Server来实现还是Oracle来实现就不知道了。
抽象工厂模式可以很方便地切换两个数据库访问的代码,但是如果需求来自增加功能,比如现在要增加项目表Project, 需要改动哪些地方?
至少要增加三个类,IProject、SqlserverProject、OracleProject,还需要更改IFactory、 SqlserverFactory和OracleFactory才可以完全实现。这非常糟糕。还有就是程序类显然不会是只有一个,有很多地方都在使用IUser或 IDepartment,而这样的设计,其实在每一个类的开始都需要声明IFactoiy fcctoiy = new SqlserverFactory(), 如果我有100个调用数据库访问的类,是不是就要更改100次IFactory factory = new OracleFactory()这样 的代码才行?这不能解决更改数据库访问时,改动一处就完全更改的要求。
为了解决第一个问题:我们得先了解Java的反射机制:https://www.cnblogs.com/jwen1994/p/10141460.html
用反射+抽象工厂的数据访问程序
DataAccess类,用反射技术,取代IFactory、SqlserverFactory和OracleFactory
public class DataAccess { private static final String packageName="com.jwen.model2";//包名 private static final String db="Sqlserver";//哪种数据库 //获取当前线程的类加载器 private static ClassLoader loader = Thread.currentThread().getContextClassLoader(); public static IUser createUser() throws Exception{ //通过类加载器加载对象 Class clazz = loader.loadClass(packageName+"."+db+"User"); //获取类的默认构造器对象并通过它实例化类 IUser iuser = (IUser) clazz.getDeclaredConstructor(null).newInstance(); return iuser; } public static IDepartment createDepartment() throws Exception{ //通过类加载器加载对象 Class clazz = loader.loadClass(packageName+"."+db+"Department"); //获取类的默认构造器对象并通过它实例化类 IDepartment dept = (IDepartment) clazz.getDeclaredConstructor(null).newInstance(); return dept; } }
测试方法
public class Test { public static void main(String[] args) throws Exception { User user = new User(); user.setId("1"); user.setName("张三"); Department dept = new Department(); dept.setId("1"); dept.setName("财务部"); IUser iu = DataAccess.createUser(); //插入用户 iu.insert(user); //得到ID为1的用户 User user2 = iu.getUser("1"); IDepartment id = DataAccess.createDepartment(); id.insert(dept); Department dept2 = id.getDepartment("1"); } }
输出结果:
在SQL Server 中给User 表增加一条记录
在SQL Server 中根据ID得到User表一条记录
在SQL Server 中给Department表增加一条记录
在SQL Server 中根据ID得到Department表一条记录
如果现在我们增加另一种数据库的访问,相关类的增加是不可避免的,这点无论我们用任何办法都解决不了,不过这叫扩展,开放-封闭原则告诉我们,对于扩展,我们开放。但对于修改,我们应该要尽量避免。就目前而言,我们只需要更改private static final String db="Sqlserver";为private static final String db="Oracle";就能实现数据库之间的切换(当然,类的取名要符合规范:数据库+表,比如oracle数据库中的user表,那么就应该取名为OracleUser)。
如果我们需要增加Project产品类,怎么办?
只需要增加三个与Project相关的类,再修改DataAccess,在其中增加一个public static IProject createProject()方法就可以了。
实际项目通常把数据库的配置放在配置文件中,这里我们仿照一下,用配置文件决定到底使用哪一种数据库:
新增test.properties文件,放在src下,这里设置db=Oracle
修改DataAccess类,在项目加载时根据配置文件中的配置决定使用哪一个数据库
package com.jwen.model3; import java.util.ResourceBundle; import com.jwen.model2.IDepartment; import com.jwen.model2.IUser; public class DataAccess { private static final String packageName="com.jwen.model2";//包名 private static String db="Sqlserver";//哪种数据库 //获取当前线程的类加载器 private static ClassLoader loader = Thread.currentThread().getContextClassLoader(); //默认使用SQL Server,但下面的静态代码读取配置文件信息,如果db不为空,则使用db配置的数据库 static{ ResourceBundle resource = ResourceBundle.getBundle("test");//test为属性文件名,如果是放在src下,直接用test即可,如果放在其他包中,需添加包路径,如com/jwen/model3/test String key = resource.getString("db"); if(key!=null&&key.length()>0){ db = key; } } public static IUser createUser() throws Exception{ //通过类加载器加载对象 Class clazz = loader.loadClass(packageName+"."+db+"User"); //获取类的默认构造器对象并通过它实例化类 IUser iuser = (IUser) clazz.getDeclaredConstructor(null).newInstance(); return iuser; } public static IDepartment createDepartment() throws Exception{ //通过类加载器加载对象 Class clazz = loader.loadClass(packageName+"."+db+"Department"); //获取类的默认构造器对象并通过它实例化类 IDepartment dept = (IDepartment) clazz.getDeclaredConstructor(null).newInstance(); return dept; } }
测试方法不变,运行结果如下:
在Oracle 中给User 表增加一条记录
在Oracle 中根据ID得到User表一条记录
在Oracle 中给Department表增加一条记录
在Oracle 中根据ID得到Department表一条记录
市面上很多流行框架都使用了抽象工厂模式,我们使用时特别方便,按部就班地配置就行,但了解其内涵可以帮助我们知其然并知其所以然。
抽象工厂模式介绍:http://www.runoob.com/design-pattern/abstract-factory-pattern.html
本文中工程结构如下:
以上是关于初步认知:抽象工厂模式的主要内容,如果未能解决你的问题,请参考以下文章
设计模式抽象工厂模式 ( 简介 | 适用场景 | 优缺点 | 产品等级结构和产品族 | 代码示例 )