【设计模式】抽象工厂模式

发布于:2025-09-12 ⋅ 阅读:(21) ⋅ 点赞:(0)

系列文章目录


在这里插入图片描述

我们知道数据库之间调用代码相差很大,如果我们同一个项目只需要换数据库,在不同数据库的语法不同会冲突,包括一些关键字也是不同的,我们如何用抽象工厂模式来解决呢?

最基本的数据访问程序

我们写一段原来数据访问的做法,例如新增用户和得到用户。

用户类,假设只有ID和Name两个字段,其余省略

  //用户类
    public class User{
        //用户ID
        private 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{
        //新增一个用户
        @Override
        public void insert(User user) {
            System.out.println("在SQL Server中给User变增加一条记录");
        }
        //获取一个用户信息
        @Override
        public User getUser(int id) {
            System.out.println("在SQL Server中根据用户ID得到User表一条记录");
            return null;
        }
    }

AccessUser类:用于访问Access的User。

  public class AccessUser implements IUser{
        //新增一个用户
        @Override
        public void insert(User user) {
            System.out.println("在Access中给User表增加一些记录");
        }
        //获取一个用户信息
        @Override
        public 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{

        @Override
        public IUser createUser() {
            return new SqlserverUser();
        }
    }

AccessFactory类:实现IFactory接口,实例化AccessUser。

  public class AccessFactory implements IFactory{
        @Override
        public 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{
        //部门id
        private 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{
        //新增一个部门
        @Override
        public void insert(Department department) {
            System.out.println("在SQL Server中给Department表添加一条数据");
        }
        //获取一个部门信息
        @Override
        public Department getDepartment(int id) {
            System.out.println("在Access中根据部门id得到Department表一条记录");
            return null;
        }
    }

AccessDepartment类:用于访问Access的Department。

  public class AccessDepartment implements  IDepartment{
        @Override
        public void insert(Department department) {
            System.out.println("在Access表中添加一条记录");
        }

        @Override
        public 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{

        @Override
        public IUser createUser() {
            return new SqlserverUser();
        }

        @Override
        public IDepartment createDepartment() {
            return new SqlserverDepartment();
        }
    }

AccessFactory类:实现IFactory接口,并实例化AccessFactory和AccessDepartment

  public class SqlserverFactory implements IFactory{

        @Override
        public IUser createUser() {
            return new SqlserverUser();
        }

        @Override
        public 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"; //数据库名称可以替换为Access
        private 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,解除分支判断带来的耦合。

总结

以上就是本文全部内容,本文主要向大家介绍了设计模式中的抽象工厂模式,通过更换数据库的例子介入,介绍抽象工厂模式,接着介绍了反射相关概念,以及反射和抽象工厂模式的结果运用! 感谢各位能够看到最后,如有问题,欢迎各位大佬在评论区指正,希望大家可以有所收获!创作不易,希望大家多多支持!


网站公告

今日签到

点亮在社区的每一天
去签到