• 抽象工厂模式


    在讲述这个模式之前,我们先看一个案例:模拟最基本的数据库访问:获取用户,向用户表插入记录

    //用户类,假设只有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

    本文中工程结构如下:

  • 相关阅读:
    linux 网络相关
    工作随笔
    python
    trouble-shooting
    MySQL常见问题总结
    根据 Request 获取客户端 IP
    文件上传按钮优化
    Linux中RabbitMQ安装
    linux 完全卸载MySQL
    Linux 下安装 MySQL-5.7.24
  • 原文地址:https://www.cnblogs.com/jwen1994/p/10132554.html
Copyright © 2020-2023  润新知