当前位置: 首页 > news >正文

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) 提供了更强的封装、更高的可靠性和更好的关注点分离&#xff0c;有些同学可能没注意到。 不过呢&#xff0c;也是有利有弊。由于模块化应用程序构建在依赖其他模块才能正常工作的模块网络上&#xff0c;因此在许多情况下&#xff0c;模块彼此紧密耦合…...

支持https访问

文章目录 1. 打开自己的云服务器的 80 和 443 端口2. 安装 nginx3. 安装 snapd4. 安装 certbot5. 生成证书6. 拷贝生成的证书到项目工作目录7. 修改 main.go 程序如下8. 编译程序9. 启动程序10. 使用 https 和端口 8081 访问页面成功11. 下面修改程序&#xff0c;支持 https 和…...

JavaScript 中常用简写技巧总结

平时我们写代码时最高级的境界是自己写的东西别人看不懂&#xff01;哈哈哈&#xff01;分享一些自己常用的js简写技巧&#xff0c;长期更新&#xff0c;会着重挑选一些实用的简写技巧&#xff0c;使自己的代码更简洁优雅~ 这里只会收集一些大多数人不知道的用法&#xff0c;但…...

第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命令查看后&#xff0c;可以查看是哪一个进程占用cpu较高&#xff0c;上图所示的进程为&#xff1a;40940 3.查看进程中的线程信息 4.可以根据进程 id 找到有问题的线程&a…...

使用公网访问内网IIS网站服务器【无需公网IP】

使用公网访问内网IIS网站服务器【无需公网IP】 文章目录 使用公网访问内网IIS网站服务器【无需公网IP】前言1. 注册并安装cpolar2. 创建隧道映射3. 获取公网地址 前言 这里介绍通过内网穿透&#xff0c;实现公网访问内网IIS网站服务器。 都知道&#xff0c;现在基本不会被分配…...

Vim学习(二)—— 编译C程序

打开终端&#xff0c;这里以MobaXterm为例&#xff0c; 邮件创建新的空文件并命名&#xff0c; 然后cd到对应路径下&#xff0c;用 vim hello.cvim打开创建的文件&#xff0c;进入编辑模式&#xff0c;编辑完程序后按Esc退出编辑模式&#xff0c;输入 :wq保存并退出&#xf…...

【maven】常见命令

文章目录 1. 打包编译时跳过测试2.显示maven依赖树3. 显示maven依赖列表4. 下载依赖包的源码5. 安装本地jar到本地仓库 1. 打包编译时跳过测试 mvn clean install -DskipTests mvn clean install -Dmaven.test.skiptrueDskipTests&#xff0c;不执行测试用例&#xff0c;但编译…...

vue单项数据传输流式回复功能,post传值可关闭请求(@microsoft/fetch-event-source)

需求&#xff1a;实现一个类似于文心一言ai回复功能&#xff0c;一个字一个字往外蹦&#xff0c;不使用websocket还有什么其他方案呢&#xff1f;经过查询有一个 microsoft/fetch-event-source单向传输协议&#xff08;服务端传输客户端&#xff09;。废话不多说&#xff0c;上…...

“深入探究JVM内部机制:理解Java虚拟机的工作原理“

标题&#xff1a;深入探究JVM内部机制&#xff1a;理解Java虚拟机的工作原理 摘要&#xff1a;本文将深入分析Java虚拟机&#xff08;JVM&#xff09;的工作原理&#xff0c;包括类加载、内存管理、垃圾回收和即时编译等方面。通过详细解释这些概念&#xff0c;并给出示例代码…...

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. 两个数组的交集排序&#xff0b;双指针数组实现哈希表unordered_setunordered_map 350. 两个数组的交集Ⅱ排序 双指针数组实现哈希表unordered_map 349. 两个数组的交集 题目链接&#xff1a;349. 两个数组的交集 题目内容如下&#xff0c;理解题意&#xff1a…...

JVM元空间溢出的排除思路

背景&#xff1a; java的应用我们为了防止元空间的无限扩展&#xff0c;一般都会设置MaxMetaSpace参数&#xff0c;一般来说只要这个值是512M或者1G左右就足够了&#xff0c;不过今天遇到一个meta空间溢出问题&#xff0c;简单记录下排除的思路 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…...

手机两个卡槽的正确使用方法,您用对了吗?

手机上有两个卡槽&#xff0c;该如何搭配才能使话费降到最低&#xff1f;你又是怎么搭配的&#xff1f; 这篇文章小编就来告诉你&#xff0c;如何在不换号的情况下&#xff0c;将自己的话费降到最低。 首先卡槽一我们就用8元保号套餐。 卡槽二&#xff0c;我们就可以办理一张…...

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&#xff0c;因此针对eslint的配置做了一下总结。 引入ESlint、pritter 安装插件&#xff0c;执行以下命令 // eslint // prettier // eslint-plugin-vue // eslint-config-prettier // eslint-plugin-prettier yarn ad…...

【C++学习手札】一文带你初识运算符重载

食用指南&#xff1a;本文在有C基础的情况下食用更佳 &#x1f340;本文前置知识&#xff1a; C类 ♈️今日夜电波&#xff1a;クリームソーダとシャンデリア—Edo_Ame江户糖 1:20 ━━━━━━️&#x1f49f;──────── 3:40 …...

生成xcframework

打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式&#xff0c;可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

springboot 百货中心供应链管理系统小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;百货中心供应链管理系统被用户普遍使用&#xff0c;为方…...

Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?

Golang 面试经典题&#xff1a;map 的 key 可以是什么类型&#xff1f;哪些不可以&#xff1f; 在 Golang 的面试中&#xff0c;map 类型的使用是一个常见的考点&#xff0c;其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路

进入2025年以来&#xff0c;尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断&#xff0c;但全球市场热度依然高涨&#xff0c;入局者持续增加。 以国内市场为例&#xff0c;天眼查专业版数据显示&#xff0c;截至5月底&#xff0c;我国现存在业、存续状态的机器人相关企…...

什么是Ansible Jinja2

理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具&#xff0c;可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板&#xff0c;允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板&#xff0c;并通…...

html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码

目录 一、&#x1f468;‍&#x1f393;网站题目 二、✍️网站描述 三、&#x1f4da;网站介绍 四、&#x1f310;网站效果 五、&#x1fa93; 代码实现 &#x1f9f1;HTML 六、&#x1f947; 如何让学习不再盲目 七、&#x1f381;更多干货 一、&#x1f468;‍&#x1f…...

在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)

考察一般的三次多项式&#xff0c;以r为参数&#xff1a; p[z_, r_] : z^3 (r - 1) z - r; roots[r_] : z /. Solve[p[z, r] 0, z]&#xff1b; 此多项式的根为&#xff1a; 尽管看起来这个多项式是特殊的&#xff0c;其实一般的三次多项式都是可以通过线性变换化为这个形式…...

MySQL JOIN 表过多的优化思路

当 MySQL 查询涉及大量表 JOIN 时&#xff0c;性能会显著下降。以下是优化思路和简易实现方法&#xff1a; 一、核心优化思路 减少 JOIN 数量 数据冗余&#xff1a;添加必要的冗余字段&#xff08;如订单表直接存储用户名&#xff09;合并表&#xff1a;将频繁关联的小表合并成…...

leetcode73-矩阵置零

leetcode 73 思路 记录 0 元素的位置&#xff1a;遍历整个矩阵&#xff0c;找出所有值为 0 的元素&#xff0c;并将它们的坐标记录在数组zeroPosition中置零操作&#xff1a;遍历记录的所有 0 元素位置&#xff0c;将每个位置对应的行和列的所有元素置为 0 具体步骤 初始化…...

李沐--动手学深度学习--GRU

1.GRU从零开始实现 #9.1.2GRU从零开始实现 import torch from torch import nn from d2l import torch as d2l#首先读取 8.5节中使用的时间机器数据集 batch_size,num_steps 32,35 train_iter,vocab d2l.load_data_time_machine(batch_size,num_steps) #初始化模型参数 def …...