Java 9 模块解耦的设计策略

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

原文地址:https://www.cnblogs.com/liululee/p/11015821.html

时间: 2024-10-10 00:29:49

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

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

推荐一篇好的文章介绍java日志系统框架的设计的文章:http://soft.chinabyte.com/database/438/11321938.shtml 文章内容总结: 日志系统对跟踪调试.程序状态记录.数据恢复等功能有重要作用 日志系统一般作为服务进程或者系统调用存在,我们一般程序中使用系统调用 常用日志系统包括log4j的简单介绍 日志系统的系统架构 日志系统的信息分级 日志输出的设计 下面是全文的引用: 在Java领域,存在大量的日志组件,open-open收录了21个日志组件.日

基于JAVA的邮件客户端的设计和实现

获取项目源文件,技术交流与指导联系Q:1225467431 摘  要 Java是Sun Microsystem公司推出的新一代面向对象和面向网络的程序设计语言,特别适合于Internet/Intranet上的应用软件开发,因此也把Java语言称为新一代网络程序设计语言.Java语言将面向对象.多线程.安全和网络等特征集于一身,为软件开发人员提供了很好的程序设计环境,当今企业级计算和应用中相当成熟和稳定的平台,在这个领域中不可否认地占据着领导地位.JBuilder是Borland公司推出的Java

互联网产品消息推送设计策略(转)

在移动互联时代,消息推送越来越受到各个APP的重视,本文就以互金产品为例阐述消息推送的几个类别以及应用的场景方式.运营策略,希望对你有益. 在之前一文中,笔者概括性的介绍了通知功能是互金理财平台的一个基础但重要的功能.消息推送能将个人账户相关.平台相关内容送达终端用户,是为互联网产品一个重要的功能.在移动互联网时代,移动客户端出现寡头效应,消息推送愈发重要,而在互金产品中尤甚. 因此本文将开始重点阐述互金产品消息推送的类别.场景.方式和前后端推送设计策略以及运营策略. 1 定义 本文所指的"互金

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

先看一幅图吧: 这幅图清晰地表达了六大设计原则,但仅限于它们叫什么名字而已,它们具体是什么意思呢?下面我将从原文.译文.理解.应用,这四个方面分别进行阐述. 1.单一职责原则(Single Responsibility Principle - SRP) 原文:There should never be more than one reason for a class to change. 译文:永远不应该有多于一个原因来改变某个类. 理解:对于一个类而言,应该仅有一个引起它变化的原因.说白了就是

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

看看这篇针对Java开发人员的SOLID设计原则简介.抽丝剥茧,细说架构那些事——[优锐课] 当你刚接触软件工程时,这些原理和设计模式不容易理解或习惯.我们都遇到了问题,很难理解SOLID + DP的思想,甚至很难正确实施它们.确实,“为什么要SOLID?”的整个概念,以及如何实施设计模式,这需要时间和大量实践. 我可以说实话,关于SOLID设计模式以及TDD等其他领域,从本质上讲,它们很难教.很难以正确的方式将所有这些知识和信息传授给年轻人. 让SOLID 变得容易 在本文中,我将以尽可能简单

2.35 Java基础总结①抽象②接口③设计抽象类和接口的原则④接口和抽象类的区别

java基础总结①抽象②接口③设计抽象类和接口的原则④接口和抽象类的区别 一.抽象 abstract作用:不能产生对象,充当父类,强制子类正确实现重写方法和类相比仅有的改变是不能产生对象,其他的都有,包括构造.属性等等任何一个类只要有一个抽象的方法就成了抽象类 抽象方法 public abstract A();①方法是抽象的,这个类也是抽象的:②子类必须重写抽象方法,除非子类也是抽象类 抽象类可以没有抽象方法,但一般不这么设计 二.接口 interface 接口也是Java的一种引用数据类型(J

电子商务(电销)平台中用户模块(User)数据库设计明细

以下是自己在电子商务系统设计中的订单模块的数据库设计经验总结,而今发表出来一起分享,如有不当,欢迎跟帖讨论~ 用户基础表(user_base)|-- 自动编号 (user_id)|-- 用户名 (user_name)|-- 手机号码|-- 电子邮件|-- 登录密码 (password)|-- 用户状态 (status) 用户开放登录帐号表|-- 自动编号|-- 用户编号|-- 腾讯QQ号码 (qq)|-- 微信号码 (wechat)|-- 淘宝帐号 (taobao)|-- Skype (skyp

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

Java Socket 网络编程心跳设计概念 1.一般是用来判断对方(设备,进程或其它网元)是否正常动行,一 般采用定时发送简单的通讯包,如果在指定时间段内未收到对方响应,则判断对方已经当掉.用于检测TCP的异常断开.一般是用来判断对方(设备,进程或其它 网元)是否正常动行,一般采用定时发送简单的通讯包,如果在指定时间段内未收到对方响应,则判断对方已经当掉.用于检测TCP的异常断开.基本原因是服务 器端不能有效的判断客户端是否在线也就是说,服务器无法区分客户端是长时间在空闲,还是已经掉线的情况.

电源模块的PCB设计

[导读]电源电路是一个电子产品的重要组成部分,电源电路设计的好坏,直接牵连产品性能的好坏.而电源模块最重要的是电源电路的PCB设计,本文就为大家讲解两个电源模块电路及其PCB布局,万变不离其宗,只要弄通这两个电路和PCB布局,其它的也了然于胸了. 为什么要学习电源电路的设计? ? 电源电路是一个电子产品的重要组成部分,电源电路设计的好坏,直接牵连产品性能的好坏. 电源电路的分类 我们电子产品的电源电路主要有线性电源和高频开关电源.从理论上讲,线性电源是用户需要多少电流,输入端就要提供多少电流;开