您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

工厂模式进阶用法,如何动态选择对象?

时间:2023-03-10 11:29:12  来源:今日头条  作者:JAVA旭阳


前言

工厂设计模式可能是最常用的设计模式之一,我想大家在自己的项目中都用到过。可能你会不屑一顾,但这篇文章不仅仅是关于工厂模式的基本知识,更是讨论如何在运行时动态选择不同的方法进行执行,你们可以看看是不是和你们项目中用的一样?

小菜鸟的问题

直接上例子说明,设计一个日志记录的功能,但是支持记录到不同的地方,例如:

  • 内存
  • 磁盘上的文件
  • 数据库
  • 百度网盘等远程存储服务

面对这么一个需求,你会怎么做呢?我们先来看看小菜鸟的做法吧。

  1. 小菜鸟创建了一个Logger类
class Logger {
    public void log(String message, String loggerMedium) {}
}
  1. 小菜鸟想都不想,直接一通if else。
class Logger {
    public void log(String message, String loggerMedium) {
        if (loggerMedium.equals("MEMORY")) {
            logInMemory(message);
        } else if (loggerMedium.equals("FILE")) {
            logOnFile(message);
        } else if (loggerMedium.equals("DB")) {
            logToDB(message);
        } else if (loggerMedium.equals("REMOTE_SERVICE")) {
            logToRemote(message);
        }
    }

    private void logInMemory(String message) {
        // Implementation
    }

    private void logOnFile(String message) {
        // Implementation
    }

    private void logToDB(String message) {
        // Implementation
    }

    private void logToRemote(String message) {
        // Implementation
    }
}

现在突然说要增加一种存储介质FLASH_DRIVE,就要改了这个类?不拍改错吗?也不符合“开闭原则”,而且随着存储介质变多,类也会变的很大,小菜鸟懵逼了,不知道怎么办?

有没有更好的方法呢?

这时候小菜鸟去找你帮忙,你一顿操作,改成了下面这样:

class InMemoryLog {
    public void logToMemory(String message) {
        // Implementation
    }
}

class FileLog {
    public void logToFile(String message) {
        //Implementation
    }
}

class DBLog {
    public void logToDB(String message) {
        // Implementation
    }
}

class RemoteServiceLog {
    public void logToService(String message) {
        // Implementation
    }
}

class Logger {
    private InMemoryLog mLog;
    private FileLog fLog;
    private DBLog dbLog;
    private RemoteServiceLog sLog;
    
    public Logger() {
        mLog = new InMemoryLog();
        fLog = new FileLog();
        dbLog = new DBLog();
        sLog = new RemoteServiceLog();
    }

    public void log(String message, String loggerMedium) {
        if (loggerMedium.equals("MEMORY")) {
            mLog.logToMemory(message);
        } else if (loggerMedium.equals("FILE")) {
            fLog.logToFile(message);
        } else if (loggerMedium.equals("DB")) {
            dbLog.logToDB(message);
        } else if (loggerMedium.equals("REMOTE_SERVICE")) {
            sLog.logToService(message);
        }
    }
}

在这个实现中,你已经将单独的代码分离到它们对应的文件中,但是Logger类与存储介质的具体实现紧密耦合,如FileLog、DBLog等。随着存储介质的增加,类中将引入更多的实例Logger。

还有什么更好的办法吗?

你想了想,上面的实现都是直接写具体的实现类,是面向实现编程,更合理的做法是面向接口编程,接口意味着协议,契约,是一种更加稳定的方式。

  1. 定义一个日志操作的接口
public interface LoggingOperation {
    void log(String message);
}
  1. 实现这个接口
class InMemoryLog implements LoggingOperation {
    public void log(String message) {
        // Implementation
    }
}

class FileLog implements LoggingOperation {
    public void log(String message) {
        //Implementation
    }
}

class DBLog implements LoggingOperation {
    public void log(String message) {
        // Implementation
    }
}

class RemoteServiceLog implements LoggingOperation {
    public void log(String message) {
        // Implementation
    }
}
  1. 你定义了一个类,据传递的参数,在运行时动态选择具体实现,这就是所谓的工厂类,不过是基础版。
class LoggerFactory {
    public static LoggingOperation getInstance(String loggerMedium) {
        LoggingOperation op = null;
        switch (loggerMedium) {
            case "MEMORY":
                op = new InMemoryLog();
                break;
            case "FILE":
                op = new FileLog();
                break;
            case "DB":
                op = new DBLog();
                break;
            case "REMOTE_SERVICE":
                op = new RemoteServiceLog();
                break;
        }

        return op;
    }
}
  1. 现在你的 Logger类的实现就是下面这个样子了。
class Logger {
    public void log(String message, String loggerMedium) {
        LoggingOperation instance = LoggerFactory.getInstance(loggerMedium);
        instance.log(message);
    }
}

这里的代码变得非常统一,创建实际存储实例的责任已经转移到LoggerFactory,各个存储类只实现它们如何将消息记录到它们的特定介质,最后该类Logger只关心通过LoggerFactory将实际的日志记录委托给具体的实现。这样,代码就很松耦合了。你想要添加一个新的存储介质,例如FLASH_DRIVE,只需创建一个实现LoggingOperation接口的新类并将其注册到LoggerFactory中就好了。这就是工厂模式可以帮助您动态选择实现的方式。

还能做得更好吗?

你已经完成了一个松耦合的设计,但是想象一下假如有数百个存储介质的场景,所以我们最终会在工厂类LoggerFactory中的switch case部分case数百个。这看起来还是很糟糕,如果管理不当,它有可能成为技术债务,这该怎么办呢?

摆脱不断增长的if else或者 switch case的一种方法是维护类中所有实现类的列表,LoggerFactory代码如下所示:

class LoggerFactory {
    private static final List<LoggingOperation> instances = new ArrayList<>();

    static {
        instances.addAll(Arrays.asList(
                new InMemoryLog(),
                new FileLog(),
                new DBLog(),
                new RemoteServiceLog()
        ));
    }

    public static LoggingOperation getInstance(ApplicationContext context, String loggerMedium) {
        for(LoggingOperation op : instances) {
            // 比如判断StrUtil.equals(loggerMedium, op.getType()) op本身添加一个type
        }

        return null;
    }
}

但是请注意,还不够,在所有上述实现中,无论if else、switch case 还是上面的做法,都是让存储实现与LoggerFactory紧密耦合的。你添加一种实现,就要修改LoggerFactory,有什么更好的做法吗?

逆向思维一下,我们是不是让具体的实现主动注册上来呢?通过这种方式,工厂不需要知道系统中有哪些实例可用,而是实例本身会注册并且如果它们在系统中可用,工厂就会为它们提供服务。具体代码如下:

class LoggerFactory {
    private static final Map<String, LoggingOperation> instances = new HashMap<>();

    public static void register(String loggerMedium, LoggingOperation instance) {
        if (loggerMedium != null && instance != null) {
            instances.put(loggerMedium, instance);
        }
    }

    public static LoggingOperation getInstance(String loggerMedium) {
        if (instances.contAInsKey(loggerMedium)) {
            return instances.get(loggerMedium);
        }
        return null;
    }
}

在这里,LoggerFactory提供了一个register注册的方法,具体的存储实现可以调用该方法注册上来,保存在工厂的instancesmap对象中。

我们来看看具体的存储实现注册的代码如下:

class RemoteServiceLog implements LoggingOperation {
    static {
        LoggerFactory.register("REMOTE", new RemoteServiceLog());
    }

    public void log(String message) {
        // Implementation
    }
}

由于注册应该只发生一次,所以它发生在static类加载器加载存储类时的块中。

但是又有一个问题,默认情况下JVM不加载类RemoteServiceLog,除非它由应用程序在外部实例化或调用。因此,尽管存储类有注册的代码,但实际上注册并不会发生,因为没有被JVM加载,不会调用static代码块中的代码, 你又犯难了。

你灵机一动,LoggerFactory是获取存储实例的入口点,能否在这个类上做点文章,就写下了下面的代码:

class LoggerFactory {
    private static final Map<String, LoggingOperation> instances = new HashMap<>();

    static {
        try {
            loadClasses(LoggerFactory.class.getClassLoader(), "com.alvin.storage.impl");
        } catch (Exception e) {
            // log or throw exception.
        }
    }

    public static void register(String loggerMedium, LoggingOperation instance) {
        if (loggerMedium != null && instance != null) {
            instances.put(loggerMedium, instance);
        }
    }

    public static LoggingOperation getInstance(String loggerMedium) {
        if (instances.containsKey(loggerMedium)) {
            return instances.get(loggerMedium);
        }
        return null;
    }

    private static void loadClasses(ClassLoader cl, String packagePath) throws Exception {

        String dottedPackage = packagePath.replaceAll("[/]", ".");

        URL upackage = cl.getResource(packagePath);
        URLConnection conn = upackage.openConnection();

        String rr = IOUtils.toString(conn.getInputStream(), "UTF-8");

        if (rr != null) {
            String[] paths = rr.split("n");

            for (String p : paths) {
                if (p.endsWith(".class")) {
                    Class.forName(dottedPackage + "." + p.substring(0, p.lastIndexOf('.')));
                }

            }
        }
    }
}

在上面的实现中,你使用了一个名为loadClasses的方法,该方法扫描提供的包名称com.alvin.storage.impl并将驻留在该目录中的所有类加载到类加载器。以这种方式,当类加载时,它们的static块被初始化并且它们将自己注册到LoggerFactory中。

如何在 SpringBoot 中实现此技术?

你突然发现你的应用是springboot应用,突然想到有更方便的解决方案。

因为你的存储实现类都被标记上注解@Component,这样 Spring 会在应用程序启动时自动加载类,它们会自行注册,在这种情况下你不需要使用loadClasses功能,Spring 会负责加载类。具体的代码实现如下:

class LoggerFactory {
    private static final Map<String, Class<? extends LoggingOperation>> instances = new HashMap<>();

    public static void register(String loggerMedium, Class<? extends LoggingOperation> instance) {
        if (loggerMedium != null && instance != null) {
            instances.put(loggerMedium, instance);
        }
    }

    public static LoggingOperation getInstance(ApplicationContext context, String loggerMedium) {
        if (instances.containsKey(loggerMedium)) {
            return context.getBean(instances.get(loggerMedium));
        }
        return null;
    }
}

getInstance需要传入ApplicationContext对象,这样就可以根据类型获取具体的实现了。

修改所有存储实现类,如下所示:

import org.springframework.stereotype.Component;

@Component
class RemoteServiceLog implements LoggingOperation {
    static {
        LoggerFactory.register("REMOTE", RemoteServiceLog.class);
    }

    public void log(String message) {
        // Implementation
    }
}

总结

我们通过一个例子,不断迭代带大家理解了工厂模式,工厂模式是一种创建型设计模式,用于创建同一类型的不同实现对象。我们来总结下这种动态选择对象工厂模式的优缺点。

优点:

  • 容易管理。在添加新的存储类时,只需将该类放入特定包中,在static代码块中注册它自己到工厂中。
  • 松耦合,当您添加新的存储实现时,您不需要在工厂类中进行任何更改。
  • 遵循SOLID编程原则。

缺点:

  • 如果是用原生通过类加载的方式,代价比较大,因为它涉及 I/O 操作。但是如果使用的是SpringBoot,则无需担心,因为框架本身会调用组件。
  • 需要额外编写一个static块,注册自己到工厂中,一不小心就遗漏了。


Tags:   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
新增融券再启动暂停键,有头部券商融券池全部收回!融券余额已较年初下降近四成
4月11日,A股市场触底反弹。其中,有一则消息是触发市场反弹的重要原因:据称,多家券商暂停新增融券通券源,拟阶段性临停融券通券源每日新增投放。《每日经济新闻》向某华东头部券商...【详细内容】
2024-04-11  Search:   点击:(2)  评论:(0)  加入收藏
16个Redis常见使用场景总结
来源:blog.csdn.net/qq_39938758/article/details/105577370目录 缓存 数据共享分布式 分布式锁 全局ID 计数器 限流 位统计 购物车 用户消息时间线timeline 消息...【详细内容】
2024-04-11  Search:   点击:(2)  评论:(0)  加入收藏
一篇文章教会你使用Python中三种简单的函数
所谓函数,就是指:把某些特定功能的代码组成为一个整体,这个整体就叫做函数。一、函数简介所谓函数,就是指:把某些特定功能的代码组成为一个整体,这个整体就叫做函数。二、函数定义...【详细内容】
2024-04-11  Search:   点击:(2)  评论:(0)  加入收藏
聊聊Rust里面的数据类型
嘿,朋友们!今天我们来聊聊Rust里面的数据类型。你知道吗?Rust的数据类型可是很重要的哦,它们帮助我们定义变量和函数可以处理什么样的数据。基本数据类型首先,让我们来看看Rust提...【详细内容】
2024-04-11  Search:   点击:(2)  评论:(0)  加入收藏
C++中的外部模板及其在当前编译文件中的实例化
在C++中,模板是一种泛型编程的工具,它允许程序员以一种类型无关的方式编写代码。然而,模板的一个常见问题是它们会导致编译时间增加,特别是在大型项目中,当多个源文件包含相同的...【详细内容】
2024-04-11  Search:   点击:(2)  评论:(0)  加入收藏
一篇文章带你了解Python的分布式进程接口
在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。一、前言在Thread和Process中,应当优...【详细内容】
2024-04-11  Search:   点击:(2)  评论:(0)  加入收藏
网络安全行业的春天何时来?
2023年下半年开始,网络安全从业人员都感受到了网安行业的寒冬,但是其实前奏并不是此刻,只是涉及到大量裁员关乎自身而人人感同身受。从近五年各个网络安全上市公司财报可以发现...【详细内容】
2024-04-11  Search:   点击:(2)  评论:(0)  加入收藏
Linux获取Redis 性能指标方法
一、监控指标&Oslash; 性能指标:Performance&Oslash; 内存指标: Memory&Oslash; 基本活动指标:Basic activity&Oslash; 持久性指标: Persistence&Oslash; 错误指标:Error二、监...【详细内容】
2024-04-11  Search:   点击:(3)  评论:(0)  加入收藏
Redis与缓存一致性问题
缓存一致性问题是在使用缓存系统,如Redis时经常遇到的问题。当数据在原始数据源(如数据库)中发生变化时,如何确保缓存中的数据与数据源保持一致,是开发者需要关注的关键问题。一...【详细内容】
2024-04-11  Search:   点击:(2)  评论:(0)  加入收藏
10余所高校公布强基计划,今年有哪些变化?
今天,中国人民大学、中国农业大学、复旦大学、武汉大学、山东大学、吉林大学、重庆大学、大连理工大学发布了2024年强基计划招生简章。目前,已有10余所高校发布了招生简章。它...【详细内容】
2024-04-11  Search:   点击:(2)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(14)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(24)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(25)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(57)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(68)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(72)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(89)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(106)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(98)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(75)  评论:(0)  加入收藏
相关文章
    无相关信息
站内最新
站内热门
站内头条