【设计模式】抽象工厂模式
系列文章目录
文章目录
- 系列文章目录
- 最基本的数据访问程序
- 工厂方法模式的数据访问程序
- 抽象工厂模式的数据访问程序
- 抽象工厂模式
- 抽象工厂模式的优点与缺点
- 用简单工厂来改进抽象工厂
- 用反射 + 抽象工厂的数据访问程序
- 用反射 + 配置文件实现数据访问程序
- 总结
我们知道数据库之间调用代码相差很大,如果我们同一个项目只需要换数据库,在不同数据库的语法不同会冲突,包括一些关键字也是不同的,我们如何用抽象工厂模式来解决呢?
最基本的数据访问程序
我们写一段原来数据访问的做法,例如新增用户和得到用户。
用户类,假设只有ID和Name两个字段,其余省略
//用户类public class User{//用户IDprivate int _id;public int getId(){return this._id;}public void setId(int value){this._id = value;}//用户姓名private String _name;public String getName(){return this._name;}public void setName(String value){this._name = value;}}
SqlServerUser类 ---- 用于操作User表,假设只有新增用户和得到用户方法,其余方法以及具体SQL语句省略。
//SqlServerUser类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 static void main(String[] args) {User user = new User();SqlServerUser su = new SqlServerUser();su.insert(user);su.getUser(1);}
这里之所以不能换数据库,原因就在于SqlServerUser su = new SqlServerUser()使得su这个对象被框死在SQL Server上了,他本质上也是在使用SQL Server的SQL语句代码,这里毫无疑问存在耦合。如果这里是灵活的,是多态的,那么在执行‘su.insert(user)’和’su.getUser(1)'时就不用考虑是在用SQL Server还是在用Access.
所以,我们可以用工厂方法模式来封装new SqlServerUser()所造成的变化。**工厂方法模式是定义一个用于创建对象的接口,让子类决定实例化哪一个类。 **
工厂方法模式的数据访问程序
IUser接口:用于客户端访问,解除与具体数据库访问的耦合
//用户类接口public interface IUser{public void insert(User user);public User getUser(int id);}
SqlserverUser类:用于访问SQL server的User
public class SqlserverUser implements IUser{//新增一个用户@Overridepublic void insert(User user) {System.out.println("在SQL Server中给User变增加一条记录");}//获取一个用户信息@Overridepublic User getUser(int id) {System.out.println("在SQL Server中根据用户ID得到User表一条记录");return null;}}
AccessUser类:用于访问Access的User。
public class AccessUser implements IUser{//新增一个用户@Overridepublic void insert(User user) {System.out.println("在Access中给User表增加一些记录");}//获取一个用户信息@Overridepublic User getUser(int id) {System.out.println("在Access中根据用户ID得到User表一条记录");return null;}}
IFactory接口:定义一个创建访问User表对象的抽象工厂接口
public interface IFactory{public IUser createUser();}
SqlServerFactory类:实现IFactory接口,实例化SqlserverUser。
public class SqlserverFactory implements IFactory{@Overridepublic IUser createUser() {return new SqlserverUser();}}
AccessFactory类:实现IFactory接口,实例化AccessUser。
public class AccessFactory implements IFactory{@Overridepublic IUser createUser() {return new AccessUser();}}
客户端代码:
public static void main(String[] args) {User user = new User();IFactory factory = new SqlserverFactory();IUser iu = factory.createUser();iu.insert(user);iu.getUser(1);}
现在如果非要换数据库,只需要把new SqlserverFactory()改成new AccessFactory(),此时由于多态的关系,使得声明IUser接口的对象iu事先根本不知道是在访问那个数据库,却可以在运行时很好的完成工作,这就是所谓的业务数据与数据访问的解耦
但是这样在代码里还是有指明‘new SqlserverFactory()’,所以我们在更换数据库时,更改的还是很多。另外数据库不可能只有一个user表,如果有多个表应该怎么办的,我们接下来一一为大家解释。
若有多个表:
public class Department{//部门idprivate int _id;//部门名称private String _name;public int get_id() {return _id;}public void set_id(int _id) {this._id = _id;}public String get_name() {return _name;}public void set_name(String _name) {this._name = _name;}}
此时我们需要增加好多类,但是还是有一定灵活性
抽象工厂模式的数据访问程序
增加了关于部门表的处理
IDepartment接口:用于客户端访问,解除与具体数据库访问的耦合
public interface IDepartment{public void insert(Department department);public Department getDepartment(int id);}
SqlserverDepartment类:用于访问SQL Server的Department。
public class SqlserverDepartment implements IDepartment{//新增一个部门@Overridepublic void insert(Department department) {System.out.println("在SQL Server中给Department表添加一条数据");}//获取一个部门信息@Overridepublic Department getDepartment(int id) {System.out.println("在Access中根据部门id得到Department表一条记录");return null;}}
AccessDepartment类:用于访问Access的Department。
public class AccessDepartment implements IDepartment{@Overridepublic void insert(Department department) {System.out.println("在Access表中添加一条记录");}@Overridepublic Department getDepartment(int id) {System.out.println("在Access中根据部门ID得到Department表一条记录");return null;}}
IFactory接口:定义一个创建访问Department表对象的抽象的工厂接口
public interface IFactory{public IUser createUser();public IDepartment createDepartment();}
SqlServerFactory类:实现IFactory接口,并实例化SqlserverUser和SqlserverDepartment.
public class SqlserverFactory implements IFactory{@Overridepublic IUser createUser() {return new SqlserverUser();}@Overridepublic IDepartment createDepartment() {return new SqlserverDepartment();}}
AccessFactory类:实现IFactory接口,并实例化AccessFactory和AccessDepartment
public class SqlserverFactory implements IFactory{@Overridepublic IUser createUser() {return new SqlserverUser();}@Overridepublic IDepartment createDepartment() {return new SqlserverDepartment();}}
客户端代码:
public static void main(String[] args) {User user = new User();Department department = new Department();IFactory factory = new SqlserverFactory();IUser iu = factory.createUser();iu.insert(user);iu.getUser(1);IDepartment department1 = factory.createDepartment();department1.insert(department);department1.getDepartment(1);}
结果显示:
在SQL Server中给User表增加一条记录
在SQL Server中根据用户ID得到User表一条记录
在SQL Server中给Department表添加一条数据
在SQL server中根据部门id得到Department表一条记录
这样的话,就可以做到,只需更改IFactory factory = new SqlServerFactory()为IFactory factory = new AccessFactory(),就实现了数据库访问的切换了。
这里我们已经通过需求的不断演化,重构除了一个非常重要的设计模式。现在我们数据库中有很多表,而SQL server与Access又是两大不同的分类,所以解决这种多个产品系列的问题,有一个专门的工厂模式叫抽象工厂模式。
抽象工厂模式
抽象工厂模式,提供一个创建一系列相关或相互依赖对象的接口,而不需指定它们具体的类。
抽象工厂模式UML结构图
AbstractProductA和AbstarctProductB是两个抽象产品 ,之所以抽象,是因为它们都有可能有两种不同的实现,就刚才的例子来说就是User和Department,而ProductA1、ProductA2和ProductB1、ProductB2就是对两个抽象产品的具体实现, 比如ProductA1可以理解为是SqlserverUser,而ProductB是SqlserverDepartment。
也就是说IFactory是一个抽象工厂接口,它里面应该包含所有的产品创建的抽象方法。而ConcreteFactory1和ConcreteFactory2就是具体的工厂了。 就像SqlserverFactory和AccessFactory一样。通常是在运行时刻再创建一个ConcreteFactory类的实例,这个具体的工厂再创建具有特定实现的产品对象,也就是说,为创建不同的产品对象,客户端应使用不同的具体工厂。
抽象工厂模式的优点与缺点
抽象工厂这样做的最大好处就是易于交换产品系列 ,由于具体工厂类,例如IFactory factory = new AccessFactory(),在一个应用中只需要在初始化的时候 出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。 我们的设计不能去防止需求的更改,那么我们的理想便是让改动变得最小,现在如果你要更改数据库访问,我们只需更改具体工厂就可以做到。**第二大好处是,**它让具体的创建实例过程与客户端分离,不会出现在客户代码中。
抽象工厂模式虽然可以很方便的切换两个数据库访问的代码,但是如果你的需求来自增加功能,比如我们现在要增加项目表Project,我们至少需要增加三个类,IProject、SqlserverProject、AccessProject,还需要更改IFactory、SqlserverFactory和AccessFactory才可以完全实现, 这步骤十分繁琐。另外,就是我们客户端程序类显然不会是只有一个,有很多地方都在使用IUser或IDepartment,而这样的设计,其实在每一个类的开始都需要声明IFactory factory = new SqlserverFactory(),如果我们需要有100个调用数据库访问的类,是不是就要更改100次IFactory factory = new AccessFactory(),显然这么做会很蠢,大批量的改动,是不符合编程这门艺术的。那么我们要如何修改呢?
用简单工厂来改进抽象工厂
代码示例如下:
public class DataAccess{private static String db = "Sqlserver"; //数据库名称可以替换为Access//创建用户对象工厂public static IUser createUser(){IUser result = null;switch(db){case "Sqlserver":result = new sqlserverUser();break;case "Access":result = new AccessUser();break;}return result;}//创建部门对象工厂public static IDepartment createDepartment(){IDepartment result = null;switch(db){case "Sqlserver":result = new SqlserverDepartment();break;case "Access":result = new AccessDepartment();break;}return result;}}//客户端public static void main(String[] args) {User user = new User();Department department = new Department();IUser iu = DataAccess.createUser();iu.insert(user);iu.getUser(1);IDepartment idept = DataAccess.createDepartment();idept.insert(department);idept.getDepartment(2);}
上述代码中,我们直接抛弃了IFactory、SqlserverFactory和AccessFactory三个工厂类, 取而代之用DataAccess类,事先声明db值,所以简单工厂的方法都不需要参数,这样在客户端就只需要DataAccess.createUser()和DataAccess.createDepartment()来生成具体的数据库访问类实例,客户端没有出现任何一个SQL server或Access的字样,达到了解耦的目的。
但这里如果我们要增加一个新的数据库访问Oracle,抽象工厂只需要增加OracleFactory工厂类就可以了,但如果这样的话,就只能在DataAccess类中每个方法的switch加Case了。
用反射 + 抽象工厂的数据访问程序
我们要考虑的就是可不可以不在程序里写明如果是Sqlserver就去实例化SQLServer数据库相关类,如果是Access就去实例化Access相关类这样的语句,而是根据字符串db的值去某个地方找应该要实例化的类是哪一个。
这就是我们说了一种编程方式: 依赖注入。关键如何用这种方法来解决我们的switch问题,本来依赖注入是需要专门的Ioc容器提供,比如Spring,显然当前这个程序不需要这么麻烦,我们需要了解Java技术–反射。
//反射格式
Object result = Class.forName("包名.类名").getDeclaredConstructor().newInstance()
有了反射我们获得实例可以用以下两种写法
//常规写法
IUser result= new SqlserverUser();
//反射写法
IUser result = (IUser)Class.forName("chouxianggongchang.SqlserverUser").getDeclaredCOnstructor().newInstance();
实例化的效果是一样的,但常规方法写明了要实例化SqlserverUser对象,反射的写法,在包名.类名中用的是字符串,可以用变量来处理,也就可以根据需求更换。
我们可以用DataAccess类,用反射技术,取代IFactory、SqlserverFactory和AccessFactory。
public class DataAccess{private static String assemblyName = "chouxianggongchang.SqlserverUser"; //数据库名称可以替换为Accessprivate static String db = "Sqlserver"; //数据库名称可以替换为//创建用户对象工厂public static IUser createUser(){return (IUser) getInstance(assemblyName + db + "User");}//创建部门对象工厂public static IDepartment createDepartment(){return (IDepartment)getInstance(assemblyName + db +"Department");}public static Object getIntance(String className){Object result = null;try{result = Class.forName(className).getDeclaredConstructor().newInstance();}catch (ClassNotFoundException e) {throw new RuntimeException(e);} catch (InvocationTargetException e) {throw new RuntimeException(e);} catch (InstantiationException e) {throw new RuntimeException(e);} catch (IllegalAccessException e) {throw new RuntimeException(e);} catch (NoSuchMethodException e) {throw new RuntimeException(e);}return result;}}
现在如果我们增加了Oracle数据访问,相关的类增加是不可避免的,这点我们无法改变,不过这叫扩展,开放 --封闭原则告诉我们,对于扩展,我们开放,但对于修改,我们要尽量关闭,就目前而言,我么只需要更改private static String db = “Sqlserver”;
为private static String db = “Oracle”;也就是
return (IUser)getIntance("chouxianggongchang" + "Sqlserver" + "User");return (IUser)getIntance("chouxianggongchang" + "Oracle" + "User");
这样的结果就是DataAccess.createUser()本来得到的是SqlserverUser()的实例,而现在变成OraclaUser的实例了。
当我们增加Project的产品时,我们可以增加三个与Project相关的类,再修改DataAccess,在其中增加一个public static IProject createProject()方法就可以了。现在我们在更改数据库访问时,只需去更改db这个字符串的值就可以了。
用反射 + 配置文件实现数据访问程序
添加一个db.properties文件,内容如下:
db=Sqlserver
再更改DataAccess类,添加与读取文件内容相关的包
import java.io.BufferedReader;
import java.io.FileReaderl
import java.io.IOException;
import java.util.properties;public class DataAccess{private static String assemblyName = "chouxianggongchang.SqlserverUser";public static String getDb(){String result = "";try{Properties properties = new Properties();//编译后,请将db.properties文件复制到要编译的class目录里,// 并确保下面path路径与实际db.properties文件路径是否一致,否则会报No Such file or directory错误String path = System.getProperty("chouxianggongchang") + "/db.properties";BufferReader bufferedReader =new BufferReader(new FileReader(path));properties.load(bufferReader);result = properties.getProperty("db");}catch(IOException e){e.printStackTrace();}return result;}
将来要更新数据库,无须重新编译任何代码,只需要更改配置文件就可以了 。
我们应用了反射 + 抽象工厂模式 解决了数据库访问时的可维护可扩展问题。
所以从这里来看,所有在用简单工厂的地方,都可以考虑用反射技术来去除switch或if,解除分支判断带来的耦合。
总结
以上就是本文全部内容,本文主要向大家介绍了设计模式中的抽象工厂模式,通过更换数据库的例子介入,介绍抽象工厂模式,接着介绍了反射相关概念,以及反射和抽象工厂模式的结果运用! 感谢各位能够看到最后,如有问题,欢迎各位大佬在评论区指正,希望大家可以有所收获!创作不易,希望大家多多支持!