原创

Java模块解耦的设计策略

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

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

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

最后,我们需要最低 3.8.0 版本的 Maven 编译插件。因此,为了保证我们是最新的,检查 Maven Central 以获取最新版本的 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 模式的示例代码。

正文到此结束
该篇文章的评论功能已被站长关闭