Java 模块解耦的设计策略
Java 平台模块系统 (JPMS) 提供了更强的封装、更高的可靠性和更好的关注点分离,有些同学可能没注意到。
不过呢,也是有利有弊。由于模块化应用程序构建在依赖其他模块才能正常工作的模块网络上,因此在许多情况下,模块彼此紧密耦合。
这可能会让我们认为模块化和松散耦合是不能在同一系统中共存的特性。不过呢,他们可以的!
接着,我们就来深入研究两种众所周知的设计模式,我们可以使用它们轻松解耦 Java 模块。
1、创建项目
我们弄个多模块的 Mavene 项目来演示吧。
为了保持代码简单,该项目最初将包含两个 Maven 模块,每个 Maven 模块将被包装到一个 Java 模块中。
第一个模块将包括一个服务接口以及两个实现——服务提供者。
第二个模块将使用提供程序来解析字符串值。
咱创建一个名为 Demoproject 的项目根目录,然后定义一下项目的父 POM:
<packaging>pom</packaging><modules><module>servicemodule</module><module>consumermodule</module>
</modules><build><pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>11</source><target>11</target></configuration></plugin></plugins></pluginManagement>
</build>
2、服务模块
出于演示目的,让我们使用一种快速而肮脏的方法来实现 servicemodule 模块,这样我们就可以清楚地发现此设计中出现的缺陷。
让我们将服务接口和服务提供者公开,将它们放在同一个包中并全部导出。这似乎是一个相当不错的设计选择,但正如我们稍后将看到的,它极大地提高了项目模块之间的耦合程度。
在项目的根目录下,我们将创建 servicemodule/src/main/java 目录。然后,我们需要定义包 com.baeldung.servicemodule,并在其中放置以下 TextService 接口:
public interface TextService {String processText(String text);}
TextService 接口非常简单,接着我们定义服务提供者。
在同一个包中,我们添加一个 Lowercase 实现:
public class LowercaseTextService implements TextService {@Overridepublic String processText(String text) {return text.toLowerCase();}}
现在,让我们添加一个大写实现:
public class UppercaseTextService implements TextService {@Overridepublic String processText(String text) {return text.toUpperCase();}}
最后,在 servicemodule/src/main/java 目录下,包含模块描述符 module-info.java:
module com.baeldung.servicemodule {exports com.baeldung.servicemodule;
}
3、消费者模块
现在我们需要创建一个使用我们之前创建的服务提供者之一的消费者模块。
让我们添加 com.baeldung.consumermodule.Application 类:
public class Application {public static void main(String args[]) {TextService textService = new LowercaseTextService();System.out.println(textService.processText("Hello from Baeldung!"));}
}
现在,让我们在源根目录中包含模块描述符 module-info.java,它应该是consumermodule/src/main/java:
module com.baeldung.consumermodule {requires com.baeldung.servicemodule;
}
最后,运行它。
但有一个值得注意的重要警告:我们不必要地将服务提供者耦合到消费者模块。
由于我们使提供者对外界可见,因此消费者模块能够意识到它们。
此外,这不利于使软件组件依赖于抽象。
4、服务提供者工厂
我们可以通过仅导出服务接口来轻松消除模块之间的耦合。相比之下,服务提供者不会被导出,因此对消费者模块来说仍然是隐藏的。消费者模块只能看到服务接口类型。
为了实现这一目标,我们需要:
1、将服务接口放在单独的包中,导出给外界
2、将服务提供者放在不同的包中,该包不导出
3、创建一个工厂类,并将其导出。消费者模块使用工厂类来查找服务提供者
我们可以将上述步骤概念化为设计模式的形式:公共服务接口、私有服务提供者和公共服务提供者工厂。
4.1 公共服务接口
为了清楚地了解此模式的工作原理,让我们将服务接口和服务提供者放在不同的包中。接口将被导出,但提供程序实现不会被导出。
因此,让我们将 TextService 移至一个名为 com.baeldung.servicemodule.external 的新包。
4.2 私服提供者
然后,我们同样将 LowercaseTextService 和 UppercaseTextService 移动到 com.baeldung.servicemodule.internal。
4.3 公共服务提供者工厂
由于服务提供者类现在是私有的,无法从其他模块访问,因此我们将使用公共工厂类来提供一种简单的机制,消费者模块可以使用该机制来获取服务提供者的实例。
在 com.baeldung.servicemodule.external 包中,我们定义 TextServiceFactory 类:
public class TextServiceFactory {private TextServiceFactory() {}public static TextService getTextService(String name) {return name.equalsIgnoreCase("lowercase") ? new LowercaseTextService(): new UppercaseTextService();}}
当然,我们可以使工厂类稍微复杂一些。为了简单起见,服务提供者只是根据传递给 getTextService() 方法的字符串值创建的。
现在,让我们替换 module-info.java 文件以仅导出外部包:
module com.baeldung.servicemodule {exports com.baeldung.servicemodule.external;
}
请注意,我们仅导出服务接口和工厂类。这些实现是私有的,因此它们对其他模块不可见。
4.4 应用类
现在,让我们重构 Application 类,以便它可以使用服务提供者工厂类:
public static void main(String args[]) {TextService textService = TextServiceFactory.getTextService("lowercase");System.out.println(textService.processText("Hello from Baeldung!"));
}
然后,运行它。
通过将服务接口设为公开,将服务提供者设为私有,我们可以通过简单的工厂类有效地解耦服务和消费者模块。
当然,没有任何模式是灵丹妙药。与往常一样,我们应该首先分析我们的用例是否适合。
5、服务和消费者模块
JPMS 通过provides…with 和uses 指令为开箱即用的服务和消费者模块提供支持。
因此,我们可以使用这个功能来解耦模块,而无需创建额外的工厂类。
为了让服务和消费者模块协同工作,我们需要执行以下操作:
1、将服务接口放在模块中,模块导出接口
2、将服务提供者放在另一个模块中 - 提供者被导出
3、在提供者的模块描述符中指定我们想要使用provides…with指令提供TextService实现
4、将 Application 类放置在它自己的模块中——消费者模块
5、在消费者模块的模块描述符中指定该模块是带有使用指令的消费者模块
6、使用消费者模块中的 Service Loader API 来查找服务提供者
这种方法非常强大,因为它利用了服务和消费者模块带来的所有功能。但这也有点棘手。
一方面,我们让消费者模块只依赖于服务接口,而不依赖于服务提供者。另一方面,我们甚至可以根本不定义服务提供者,应用程序仍然可以编译。
5.1 父模块
为了实现这个模式,我们还需要重构父 POM 和现有模块。
由于服务接口、服务提供者和消费者现在将位于不同的模块中,我们首先需要修改父 POM 的 部分,以反映这个新结构:
<modules><module>servicemodule</module><module>providermodule</module><module>consumermodule</module>
</modules>
5.2 服务模块
我们的 TextService 接口将返回 com.baeldung.servicemodule。
我们将相应地更改模块描述符:
module com.baeldung.servicemodule {exports com.baeldung.servicemodule;
}
5.3 提供者模块
如前所述,提供程序模块用于我们的实现,因此现在让我们将 LowerCaseTextService 和 UppercaseTextService 放在这里。我们将它们放入一个名为 com.baeldung.providermodule 的包中。
最后,我们添加一个 module-info.java 文件:
module com.baeldung.providermodule {requires com.baeldung.servicemodule;provides com.baeldung.servicemodule.TextService with com.baeldung.providermodule.LowercaseTextService;
}
5.4 消费者模块
现在,让我们重构消费者模块。首先,我们将应用程序放回 com.baeldung.consumermodule 包中。
接下来,我们将重构 Application 类的 main() 方法,以便它可以使用 ServiceLoader 类来发现适当的实现:
public static void main(String[] args) {ServiceLoader<TextService> services = ServiceLoader.load(TextService.class);for (final TextService service: services) {System.out.println("The service " + service.getClass().getSimpleName() + " says: " + service.parseText("Hello from Baeldung!"));}
}
最后,我们将重构 module-info.java 文件:
module com.baeldung.consumermodule {requires com.baeldung.servicemodule;uses com.baeldung.servicemodule.TextService;
}
然后,运行它!
正如我们所看到的,实现这种模式比使用工厂类的模式稍微复杂一些。即便如此,额外的努力也会通过更灵活、松散耦合的设计得到高度回报。
费者模块依赖于抽象,并且在运行时也很容易插入不同的服务提供者。
6、总结
我们学习了如何实现两种模式来解耦 Java 模块。
这两种方法都使消费者模块依赖于抽象,这始终是软件组件设计中所需的功能。
当然,每一种都有其优点和缺点。对于第一个,我们得到了很好的解耦,但我们必须创建一个额外的工厂类。
对于第二个,为了使模块解耦,我们必须创建一个额外的抽象模块并使用 Service Loader API 添加新的间接级别。
相关文章:
Java 模块解耦的设计策略
Java 平台模块系统 (JPMS) 提供了更强的封装、更高的可靠性和更好的关注点分离,有些同学可能没注意到。 不过呢,也是有利有弊。由于模块化应用程序构建在依赖其他模块才能正常工作的模块网络上,因此在许多情况下,模块彼此紧密耦合…...
支持https访问
文章目录 1. 打开自己的云服务器的 80 和 443 端口2. 安装 nginx3. 安装 snapd4. 安装 certbot5. 生成证书6. 拷贝生成的证书到项目工作目录7. 修改 main.go 程序如下8. 编译程序9. 启动程序10. 使用 https 和端口 8081 访问页面成功11. 下面修改程序,支持 https 和…...
JavaScript 中常用简写技巧总结
平时我们写代码时最高级的境界是自己写的东西别人看不懂!哈哈哈!分享一些自己常用的js简写技巧,长期更新,会着重挑选一些实用的简写技巧,使自己的代码更简洁优雅~ 这里只会收集一些大多数人不知道的用法,但…...
第15集丨Vue 江湖 —— 组件
目录 一、为什么需要组件1.1 传统方式编写应用1.2 使用组件方式编写应用1.3 Vue的组件管理 二、Vue中的组件1.1 基本概念1.1.1 组件分类1.1.2 Vue中使用组件的三大步骤:1.1.3 如何定义一个组件1.1.4 如何注册组件1.1.5 如何使用组件 1.2 注意点1.2.1 关于组件名1.2.2 关于组件标…...
【JVM】CPU飙高排查方案与思路
文章目录 CPU飙高排查方案与思路 CPU飙高排查方案与思路 1.使用top命令查看占用cpu的情况 2.通过top命令查看后,可以查看是哪一个进程占用cpu较高,上图所示的进程为:40940 3.查看进程中的线程信息 4.可以根据进程 id 找到有问题的线程&a…...
使用公网访问内网IIS网站服务器【无需公网IP】
使用公网访问内网IIS网站服务器【无需公网IP】 文章目录 使用公网访问内网IIS网站服务器【无需公网IP】前言1. 注册并安装cpolar2. 创建隧道映射3. 获取公网地址 前言 这里介绍通过内网穿透,实现公网访问内网IIS网站服务器。 都知道,现在基本不会被分配…...
Vim学习(二)—— 编译C程序
打开终端,这里以MobaXterm为例, 邮件创建新的空文件并命名, 然后cd到对应路径下,用 vim hello.cvim打开创建的文件,进入编辑模式,编辑完程序后按Esc退出编辑模式,输入 :wq保存并退出…...
【maven】常见命令
文章目录 1. 打包编译时跳过测试2.显示maven依赖树3. 显示maven依赖列表4. 下载依赖包的源码5. 安装本地jar到本地仓库 1. 打包编译时跳过测试 mvn clean install -DskipTests mvn clean install -Dmaven.test.skiptrueDskipTests,不执行测试用例,但编译…...
vue单项数据传输流式回复功能,post传值可关闭请求(@microsoft/fetch-event-source)
需求:实现一个类似于文心一言ai回复功能,一个字一个字往外蹦,不使用websocket还有什么其他方案呢?经过查询有一个 microsoft/fetch-event-source单向传输协议(服务端传输客户端)。废话不多说,上…...
“深入探究JVM内部机制:理解Java虚拟机的工作原理“
标题:深入探究JVM内部机制:理解Java虚拟机的工作原理 摘要:本文将深入分析Java虚拟机(JVM)的工作原理,包括类加载、内存管理、垃圾回收和即时编译等方面。通过详细解释这些概念,并给出示例代码…...
ubuntu18.04下配置muduoC++11环境
1.安装muduo依赖的编译工具及库 Cmake sudo apt-get install cmakeBoost sudo apt-get install libboost-dev libboost-test-devcurl、c-ares DNS、google protobuf sudo apt-get install libcurl4-openssl-dev libc-ares-dev sudo apt-get install protobuf-compiler libp…...
leetcode 力扣刷题 数组交集(数组、set、map都可实现哈希表)
数组交集 349. 两个数组的交集排序+双指针数组实现哈希表unordered_setunordered_map 350. 两个数组的交集Ⅱ排序 双指针数组实现哈希表unordered_map 349. 两个数组的交集 题目链接:349. 两个数组的交集 题目内容如下,理解题意:…...
JVM元空间溢出的排除思路
背景: java的应用我们为了防止元空间的无限扩展,一般都会设置MaxMetaSpace参数,一般来说只要这个值是512M或者1G左右就足够了,不过今天遇到一个meta空间溢出问题,简单记录下排除的思路 meta元空间溢出 最开始的现象…...
vue+java实现在线播放mp4视频
java: 读取本地视频文件的流然后给response的输出流 File file new File("/Users/zhangqingtian/Documents/水库/Floodforecast/static/" videoName);BufferedInputStream inputStream new BufferedInputStream(new FileInputStream(file));response.setContentT…...
手机两个卡槽的正确使用方法,您用对了吗?
手机上有两个卡槽,该如何搭配才能使话费降到最低?你又是怎么搭配的? 这篇文章小编就来告诉你,如何在不换号的情况下,将自己的话费降到最低。 首先卡槽一我们就用8元保号套餐。 卡槽二,我们就可以办理一张…...
PyTorch翻译官网教程-NLP FROM SCRATCH: CLASSIFYING NAMES WITH A CHARACTER-LEVEL RNN
官网链接 NLP From Scratch: Classifying Names with a Character-Level RNN — PyTorch Tutorials 2.0.1cu117 documentation 使用CHARACTER-LEVEL RNN 对名字分类 我们将建立和训练一个基本的字符级递归神经网络(RNN)来分类单词。本教程以及另外两个“from scratch”的自然…...
基于注意力神经网络的深度强化学习探索方法:ARiADNE
ARiADNE:A Reinforcement learning approach using Attention-based Deep Networks for Exploration 文章目录 ARiADNE:A Reinforcement learning approach using Attention-based Deep Networks for Exploration机器人自主探索(ARE)ARE的传统边界法非短视路径深度强化学习的方…...
Martin_DHCP_V3.0 (DHCP自动化泛洪攻击GUI)
Github>https://github.com/MartinxMax/Martin_DHCP_V3.0 首页 Martin_DHCP_V3.0 自动化DHCP洪泛攻击 Martin_DHCP_V3.0 使用方法 安装三方库 #python3 1.RunMe_Install_Packet.py 攻击路由器 #python3 Martin_DHCP_Attack.py 填写网卡 填写攻击次数 开始运行...
vscode vue3+vite 配置eslint
vue2webpackeslint配置 目前主流项目都在使用vue3vite,因此针对eslint的配置做了一下总结。 引入ESlint、pritter 安装插件,执行以下命令 // eslint // prettier // eslint-plugin-vue // eslint-config-prettier // eslint-plugin-prettier yarn ad…...
【C++学习手札】一文带你初识运算符重载
食用指南:本文在有C基础的情况下食用更佳 🍀本文前置知识: C类 ♈️今日夜电波:クリームソーダとシャンデリア—Edo_Ame江户糖 1:20 ━━━━━━️💟──────── 3:40 …...
React Native 开发环境搭建(全平台详解)
React Native 开发环境搭建(全平台详解) 在开始使用 React Native 开发移动应用之前,正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南,涵盖 macOS 和 Windows 平台的配置步骤,如何在 Android 和 iOS…...
【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
深度学习习题2
1.如果增加神经网络的宽度,精确度会增加到一个特定阈值后,便开始降低。造成这一现象的可能原因是什么? A、即使增加卷积核的数量,只有少部分的核会被用作预测 B、当卷积核数量增加时,神经网络的预测能力会降低 C、当卷…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...
嵌入式常见 CPU 架构
架构类型架构厂商芯片厂商典型芯片特点与应用场景PICRISC (8/16 位)MicrochipMicrochipPIC16F877A、PIC18F4550简化指令集,单周期执行;低功耗、CIP 独立外设;用于家电、小电机控制、安防面板等嵌入式场景8051CISC (8 位)Intel(原始…...
uniapp 实现腾讯云IM群文件上传下载功能
UniApp 集成腾讯云IM实现群文件上传下载功能全攻略 一、功能背景与技术选型 在团队协作场景中,群文件共享是核心需求之一。本文将介绍如何基于腾讯云IMCOS,在uniapp中实现: 群内文件上传/下载文件元数据管理下载进度追踪跨平台文件预览 二…...
Leetcode33( 搜索旋转排序数组)
题目表述 整数数组 nums 按升序排列,数组中的值 互不相同 。 在传递给函数之前,nums 在预先未知的某个下标 k(0 < k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k1], …, nums[n-1], nums[0], nu…...
