1. 概述

Java 平台模块系统 (Java Platform Module System,JPMS)提供了更强的封装、更可靠且更好的关注点分离。

但所有的这些方便的功能都需要付出代价。由于模块化的应用程序建立在依赖其他正常工作的模块的模块网上,因此在许多情况下,模块彼此紧密耦合。

这可能会导致我们认为模块化和松耦合是在同一系统中不能共存的特性。但事实上可以!

在本教程中,我们将深入探讨两种众所周知的设计模式,我们可以用它们轻松的解耦 Java 模块。

2. 父模块

为了展示用于解耦 Java 模块的设计模式,我们将构建一个多模块 Maven 项目的 demo。

为了保持代码简单,项目最初将包含两个 Maven 模块,每个 Maven 模块将被包装为 Java 模块

第一个模块将包含一个服务接口,以及两个实现——服务provider。第二个模块将使用该provider解析 String 的值。

让我们从创建名为 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>

在该父 POM 的定义中有一些值得强调的细节。

首先,该文件包含我们上面提到的两个子模块,即 servicemodule 和 comsumermodule(我们稍后详细讨论它们)。

然后,由于我们使用 Java 11,因此我们的系统至少需要 Maven 3.5.0,因为 Maven 从该版本开始支持 Java 9 及更高版本。

最后,我们需要最低 3.8.0 版本的 Maven 编译插件。因此,为了保证我们是最新的,检查 [Maven Central](search.maven.org/classic/#se… AND a%3A"maven-compiler-plugin") 以获取最新版本的 Maven 编译插件。

3. Service 模块

出于演示目的,我们使用一种快速上手的方式实现 servicemodule 模块,这样我们可以清楚的发现这种设计带来的缺陷。

让我们将 service 接口和 service provider公开,将它们放置在同一个包中并导出所有这些接口。这似乎是一个相当不错的设计选择,但我们稍后将看到,它大大的提高了项目的模块之间的耦合程度。

在项目的根目录下,我们创建 servicemodule/src/main/java 目录。然后,在定义包 com.baeldung.servicemodule,并在其中放置以下 TextService 接口:

public interface TextService {

    String processText(String text);

}

TextService 接口非常简单,现在让我们定义服务provider。在同样的包下,添加一个 Lowercase 实现:

public class LowercaseTextService implements TextService {

    @Override
public String processText(String text) {
return text.toLowerCase();
} }

现在,让我们添加一个 Uppercase 实现:

public class UppercaseTextService implements TextService {

    @Override
public String processText(String text) {
return text.toUpperCase();
} }

最后,在 servicemodule/src/main/java 目录下,让我们引入模块描述,module-info.java

module com.baeldung.servicemodule {
exports com.baeldung.servicemodule;
}

4. Consumer 模块

现在我们需要创建一个使用之前创建的服务provider之一的 consumer 模块。

让我们添加以下 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;
}

最后,从 IDE 或命令控制台中编译源文件并运行应用程序。

和我们预期的一样,我们应该看到以下输出:

hello from baeldung!

这可以运行,但有一个值得注意的重要警告:我们不必将 service provider和 consumer 模块耦合起来。

由于我们让provider对外部世界可见,consumer 模块会知道它们。

此外,这与软件组件依赖于抽象相冲突。

5. Service provider工厂

我们可以轻松的移除模块间的耦合,通过只暴露 service 接口。相比之下,service provider不会被导出,因此对 consumer 模块保持隐藏。consumer 模块只能看到 service 接口类型。

要实现这一点,我们需要:

  1. 放置 service 接口到单独的包中,该包将导出到外部世界
  2. 放置 service provider到不导出的其他包中,该包不导出
  3. 创建导出的工厂类。consumer 模块使用工厂类查找 service provider

我们可以以设计模式的形式概念化以上步骤:公共的 service 接口、私有的 service provider以及公共的 service provider工厂。

5.1. 公共的 Service 接口

要清楚的知道该模式如何运作,让我们将 service 接口和 service provider放到不同的包中。接口将被导出,但provider实现不会被导出。

因此,将 TextService 移到叫做 com.baeldung.servicemodule.external 的新包。

5.2. 私有的 Service provider

然后,类似的将 LowercaseTextService 和 UppercaseTextService 移动到 com.baeldung.servicemodule.internal

5.3. 公共的 Service Provider工厂

由于 service provider类现在是私有的且无法从其他模块访问,我们将使用公共工厂类来提供消费者模块可用于获取 service provider实例的简单机制。

在 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() 方法的 String值简单的创建 service provider。

现在,放置 module-info.java 文件只以导出 external 包:

module com.baeldung.servicemodule {
exports com.baeldung.servicemodule.external;
}

注意,我们只导出了 service 接口和工厂类。实现是私有的,因此它们对其他模块不可见。

5.4. Application 类

现在,让我们重构 Application 类,以便它可以使用 service provider工厂类:

public static void main(String args[]) {
TextService textService = TextServiceFactory.getTextService("lowercase");
System.out.println(textService.processText("Hello from Baeldung!"));
}

和预期一样,如果我们运行应用程序,可以导线相同的文本被打印到控制台:

hello from baeldung!

通过是 service 接口公开以及 service provider私有,有效的允许我们通过简单的工厂类来解耦 service 和 consumer 模块。

当然,没有一种模式是银弹。和往常一样,我们应该首先分析我们适合的情景。

6. Service 和 Consumer 模块

JPMS 通过 provides…with 和 uses 指令为 service 和 consumer 模块提供开箱即用的支持。

因此,我们可以使用该功能解耦模块,无需创建额外的工厂类。

要使 service 和 consumer 模块协同工作,我们需要执行以下操作:

  1. 将 service 接口放到导出接口的模块中
  2. 在另一个模块中放置 service provider——provider被导出
  3. 在provider的模块描述中使用 provides…with 指令指定我们我们要使用的 TextService 实现
  4. 将 Application 类放置到它自己的模块——consumer 模块
  5. 在 consumer 模块描述中使用 uses 指令指定该模块是 consumer 模块
  6. 在 consumer 模块中使用 Service Loader API 查找 service provider

该方法非常强大,因为它利用了 service 和 consumer 模块带来的所有功能。但这有一点棘手。

一方面,我们使 consumer 模块只依赖于 service 接口,不依赖 service provider。另一方面,我们甚至根本无法定义 service 应用者,但应用程序仍然可以编译。

6.1. 父模块

要实现这种模式,我们需要重构父 POM 和现有模块。

由于 service 接口、service provider以及 consumer 将存在于不同的模块,我们首先修改父 POM 的 部分,以反映新结构:

<modules>
<module>servicemodule</module>
<module>providermodule</module>
<module>consumermodule</module>
</modules>

6.2. Service 模块

TextService 接口将回到 com.baeldung.servicemodule 中。

我们将相应的更改模块描述:

module com.baeldung.servicemodule {
exports com.baeldung.servicemodule;
}

6.3. Provider模块

如上所述,provider模块是我们的实现,所以现在让我们在这里放置 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;
}

6.4. Consumer 模块

现在,重构 consumer 模块。首先,将 Application 放回 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;
}

现在,让我们运行应用程序。和期望的一样,我们应该看到以下文本打印到控制台:

The service LowercaseTextService says: hello from baeldung!

可以看到,实现这种模式比使用工厂类的稍微复杂一些。即便如此,额外的努力会获得更灵活、松耦合的设计。

consumer 模块依赖于抽象,并且在运行时也可以轻松的在不同的 service provider中切换。

7. 总结

在本教程中,我们学习了如何解耦 Java 模块的两种模式。

这两种方法都使得 consumer 模块依赖于抽象,这在软件组件设计中始终是期待的特性。

当然,每种都有其优点和缺点。对于第一种,我们获得了很好的解耦,但我们不得不创建额外的工厂类。

对于第二种,为了解耦模块,我们不得不创建额外的抽象模块并添加使用 Service Loader API 的新的中间层 。

和往常一样,本教程中的展示的所有示例都可以在 GitHub 上找到。务必查看 Service Factory 和 Provider Module 模式的示例代码。

原文链接:www.baeldung.com/java-module…

作者:Alejandro Ugarte

译者:Darren Luo

Java 9 模块解耦的设计策略的更多相关文章

  1. Java生鲜电商平台-订单模块状态机架构设计

    Java生鲜电商平台-订单模块状态机架构设计 说明:在Java生鲜电商平台中订单的状态流转业务        我们知道 一个订单会有很多种状态:临时单.已下单.待支付.待收货.待评价.已完成,退货中等 ...

  2. Java日志系统框架的设计与实现

    推荐一篇好的文章介绍java日志系统框架的设计的文章:http://soft.chinabyte.com/database/438/11321938.shtml 文章内容总结: 日志系统对跟踪调试.程 ...

  3. Java设计模式(十二) 策略模式

    原创文章,同步发自作者个人博客,http://www.jasongj.com/design_pattern/strategy/ 策略模式介绍 策略模式定义 策略模式(Strategy Pattern) ...

  4. 十年阿里java架构师的六大设计原则和项目经验

      先看一幅图吧: 这幅图清晰地表达了六大设计原则,但仅限于它们叫什么名字而已,它们具体是什么意思呢?下面我将从原文.译文.理解.应用,这四个方面分别进行阐述. 1.单一职责原则(Single Res ...

  5. 适用于Java开发人员的SOLID设计原则简介

    看看这篇针对Java开发人员的SOLID设计原则简介.抽丝剥茧,细说架构那些事——[优锐课] 当你刚接触软件工程时,这些原理和设计模式不容易理解或习惯.我们都遇到了问题,很难理解SOLID + DP的 ...

  6. Java商城秒杀系统的设计与实战视频教程(SpringBoot版)_汇总贴

    51CTO学院 Java商城秒杀系统的设计与实战视频教程(SpringBoot版) H:\BaiDu\微服务0830\2019最新 Java商城秒杀系统的设计与实战视频教程(SpringBoot版) ...

  7. Java Socket 网络编程心跳设计概念

    Java Socket 网络编程心跳设计概念   1.一般是用来判断对方(设备,进程或其它网元)是否正常动行,一 般采用定时发送简单的通讯包,如果在指定时间段内未收到对方响应,则判断对方已经当掉.用于 ...

  8. Chrome中的Device模块调式响应性设计

    Chrome中的Device模块调式响应性设计 阅读目录 启用Device模块 Device模块设置介绍 自定义预设介绍 查看media queries 触发触摸事件 回到顶部 启用Device模块 ...

  9. Java的MongoDB驱动及读写策略

    网上看见一篇博文,详细讲了MongoDB读写策略,将来生产会遇到类似的问题,转来备查. 指定新mongo实例: Mongo m = new Mongo(); Mongo m = new Mongo( ...

随机推荐

  1. 洛谷 P1342 请柬

    题目描述 在电视时代,没有多少人观看戏剧表演.Malidinesia古董喜剧演员意识到这一事实,他们想宣传剧院,尤其是古色古香的喜剧片.他们已经打印请帖和所有必要的信息和计划.许多学生被雇来分发这些请 ...

  2. *UOJ#223. 【NOI2016】国王饮水记

    $n \leq 8000$的数列,问不超过$m \leq 1e9$次操作后第一个数字最大是多少.操作:选一些数,把他们变成他们的平均值.需要保留$p \leq 3000$位小数,提供了一个小数高精度库 ...

  3. 【源码】List<T>泛型绑定repeater,以及repeater的交替绑定

    原文发布时间为:2009-10-28 -- 来源于本人的百度文章 [由搬家工具导入] 后台: using System;using System.Collections.Generic; public ...

  4. html执行.NET函数 html操作数据库 html与ashx结合

    原文发布时间为:2009-09-30 -- 来源于本人的百度文章 [由搬家工具导入] html页面执行.NET函数 html与ashx的结合 1、添加一般应用程序Handler.ashx <%@ ...

  5. c language compile process.

  6. IMAGE_OPTIONAL_HEADER32 结构作用

    IMAGE_OPTIONAL_HEADER32 结构作用 接 着我们来谈谈 IMAGE_OPTIONAL_HEADER 结构,正如名字的意思,这是一个可选映像头,是一个可选的结构,但是呢,实际上上节课 ...

  7. Program "D:\AndroidDevelopment\android-ndk-r9\ndk-build.cmd" not found in PATH

    1.问题描述 2.解决方法:修改ndk-build.cmd的配置路径, 修改成本地ndk-build.cmd所在路径,如下

  8. TopCoder SRM 675 Div1 Problem 500 LimitedMemorySeries1(分块)

    题意  给定一个长度不超过$5*10^{6}$的数列和不超过$100$个询问,每次询问这个数列第$k$小的数,返回所有询问的和 内存限制很小,小到不能存下这个数列.(数列以种子的形式给出) 时限$10 ...

  9. luogu P1880 石子合并

    题目描述 在一个园形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分. 试设计出1个算法,计算出将N堆石子合并成1 ...

  10. Java并发编程,深度探索J.U.C - AQS

    java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心. CountdownLatch 用来控制一个线程等待多个线程. 维护了一个计数器 cnt ...