Java 9 模块解耦的设计策略

1. 概述

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

但全部的这些方便的功能都须要付出代价。因为模块化的应用程序创建在依赖其余正常工做的模块的模块网上,所以在许多状况下,模块彼此紧密耦合git

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

在本教程中,咱们将深刻探讨两种众所周知的设计模式,咱们能够用它们轻松的解耦 Java 模块。apache

2. 父模块

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

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

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

让咱们从建立名为 demoproject 的项目根目录开始,定义项目的父 POM:ide

<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 的定义中有一些值得强调的细节。模块化

首先,该文件包含咱们上面提到的两个子模块,即 servicemodulecomsumermodule(咱们稍后详细讨论它们)。学习

而后,因为咱们使用 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

而后,相似的将 LowercaseTextServiceUppercaseTextService 移动到 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…withuses 指令为 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 FactoryProvider Module 模式的示例代码。

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

做者:Alejandro Ugarte

译者:Darren Luo

相关文章
相关标签/搜索