Design Strategies for Decoupling Java Modules – Java模块解耦的设计策略

最后修改: 2019年 5月 23日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

The Java Platform Module System (JPMS) provides stronger encapsulation, more reliability and better separation of concerns.

Java平台模块系统(JPMS)提供了更强的封装、更多的可靠性和更好的关注点分离。

But all these handy features come at a price. Since modularized applications are built upon a network of modules that depend on other modules to properly work, in many cases, the modules are tightly-coupled to each other.

但所有这些方便的功能都是有代价的。由于模块化的应用程序是建立在一个依赖其他模块正常工作的模块网络之上的,在许多情况下,模块之间是紧密耦合的。

This might lead us to think that modularity and loose-coupling are features that just can’t co-exist in the same system. But actually, they can!

这可能会让我们认为,模块化和松散耦合是不能在同一个系统中共存的特性。但实际上,它们可以

In this tutorial, we’ll look in depth at two well-known design patterns that we can use for easily decoupling Java modules.

在本教程中,我们将深入研究两种著名的设计模式,我们可以用它们来轻松解耦Java模块。

2. The Parent Module

2.父母模块

To showcase the design patterns that we’ll use for decoupling Java modules, we’ll build a demo multi-module Maven project.

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

To keep the code simple, the project will contain initially two Maven modules, and each Maven module will be wrapped into a Java module.

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

The first module will include a service interface, along with two implementations – the service providers. The second module will use the providers for parsing a String value.

第一个模块将包括一个服务接口,以及两个实现–服务提供者。第二个模块将使用这些提供者来解析String值。

Let’s start by creating the project’s root directory named demoproject, and we’ll define the project’s parent POM:

让我们先创建项目的根目录,命名为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>

There are a few details worth stressing in the definition of the parent POM.

在母体POM的定义中,有几个细节值得强调。

First off, the file includes the two child modules that we mentioned above, namely servicemodule and consumermodule (we’ll discuss them in detail later).

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

Next, since we’re using Java 11, we’ll need at least Maven 3.5.0 on our system, as Maven supports Java 9 and higher from that version onward.

接下来,由于我们使用的是Java 11,我们的系统至少需要Maven 3.5.0因为Maven从该版本开始支持Java 9及以上版本

Finally, we’ll also need at least version 3.8.0 of the Maven compiler plugin. So, to make sure we’re up to date we’ll check Maven Central for the latest version of the Maven compiler plugin.

最后,我们还需要至少3.8.0版本的Maven编译器插件。因此,为了确保我们是最新的,我们将检查Maven Central,查看最新版本的Maven编译器插件。

3. The Service Module

3.服务模块

For demo purposes, let’s use a quick-and-dirty approach to implement the servicemodule module, so we can clearly spot the flaws that arise with this design.

为了演示的目的,让我们用一种快速的方法来实现servicemodule模块,这样我们就可以清楚地发现这种设计产生的缺陷。

Let’s make the service interface and the service providers public, by placing them in the same package and by exporting all of them. This seems to be a fairly good design choice, but as we’ll see in a moment, it highly increases the level of coupling between the project’s modules.

让我们把服务接口和服务提供者变成公共的,把它们放在同一个包里,并把它们全部导出。这似乎是一个相当好的设计选择,但正如我们稍后所看到的,它高度增加了项目模块之间的耦合度

Under the project’s root directory, we’ll create the servicemodule/src/main/java directory. Then, we need to define the package com.baeldung.servicemodule, and place in it the following TextService interface:

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

public interface TextService {
    
    String processText(String text);
    
}

The TextService interface is really simple, so let’s now define the service providers.

TextService接口真的很简单,所以我们现在来定义服务提供者。

In the same package, let’s add a Lowercase implementation:

在同一个包中,让我们添加一个Lowercase实现。

public class LowercaseTextService implements TextService {

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

Now, let’s add an Uppercase implementation:

现在,让我们添加一个大写字母的实现。

public class UppercaseTextService implements TextService {
    
    @Override
    public String processText(String text) {
        return text.toUpperCase();
    }
    
}

Finally, under the servicemodule/src/main/java directory, let’s include the module descriptor, module-info.java:

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

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

4. The Consumer Module

4.消费者模块

Now we need to create a consumer module that uses one of the service providers that we created before.

现在我们需要创建一个消费者模块,使用我们之前创建的一个服务提供者。

Let’s add the following com.baeldung.consumermodule.Application class:

让我们添加以下com.baeldung.consumerermodule.Application类。

public class Application {
    public static void main(String args[]) {
        TextService textService = new LowercaseTextService();
        System.out.println(textService.processText("Hello from Baeldung!"));
    }
}

Now, let’s include the module descriptor, module-info.java, at the source root, which ought to be consumermodule/src/main/java:

现在,让我们把模块描述符,module-info.java,放在源代码根部,应该是consumermodule/src/main/java

module com.baeldung.consumermodule {
    requires com.baeldung.servicemodule;
}

Finally, let’s compile the source files and run the application, either from within our IDE or from a command console.

最后,让我们编译源文件并运行该应用程序,可以从我们的IDE内或从命令控制台运行。

As we might expect, we should see the following output:

正如我们可能期望的那样,我们应该看到以下输出。

hello from baeldung!

Definitively this works, but with one important caveat worth noting: we’re unnecessarily coupling the service providers to the consumer module.

这无疑是可行的,但有一个值得注意的重要注意事项。我们不必要地将服务提供者与消费者模块耦合在一起

Since we’re making the providers visible to the outside world, consumer modules are aware of them.

既然我们让供应商对外界可见,那么消费者模块就会意识到它们。

Moreover, this fights against making software components depend on abstractions.

此外,这也是对使软件组件依赖于抽象的斗争。

5. Service Provider Factory

5.服务供应商工厂

We can easily remove the coupling between the modules by exporting only the service interface. By contrast, the service providers are not exported, thus remaining hidden from the consumer modules. The consumer modules only see the service interface type.

我们可以通过只导出服务接口轻松地消除模块间的耦合。相比之下,服务提供者没有被导出,因此对消费者模块来说是隐藏的。消费者模块只看到服务接口类型。

To accomplish this, we need to:

为了实现这一目标,我们需要。

  1. Place the service interface in a separate package, which is exported to the outside world
  2. Place the service providers in a different package, which is not exported
  3. Create a factory class, which is exported. The consumer modules use the factory class to lookup the service providers

We can conceptualize the steps above in the form of a design pattern: public service interface, private service providers, and public service provider factory.

我们可以以设计模式的形式将上述步骤概念化。公共服务接口,私人服务提供者,以及公共服务提供者工厂

5.1. Public Service Interface

5.1.公共服务接口

To clearly see how this pattern works, let’s place the service interface and the service providers in different packages. The interface will be exported, but the provider implementations won’t.

为了清楚地看到这种模式是如何工作的,让我们把服务接口和服务提供者放在不同的包中。该接口将被导出,但提供者的实现不会被导出。

So, let’s move TextService to a new package we’ll call com.baeldung.servicemodule.external.

所以,让我们把TextService移到一个新的包里,我们称之为com.baeldung.servicemodule.external

5.2. Private Service Providers

5.2.私营服务提供者

Then, let’s similarly move our LowercaseTextService and UppercaseTextService to com.baeldung.servicemodule.internal.

然后,让我们同样地将我们的LowercaseTextServiceUppercaseTextService移到com.baeldung.servicemodule.internal.

5.3. Public Service Provider Factory

5.3.公共服务提供者工厂

Since the service provider classes are now private and can’t be accessed from other modules, we’ll use a public factory class to provide a simple mechanism that consumer modules can use for getting instances of the service providers.

由于服务提供者类现在是私有的,不能被其他模块访问,我们将使用一个公共工厂类来提供一个简单的机制,消费者模块可以用它来获得服务提供者的实例

In the com.baeldung.servicemodule.external package, let’s define the following TextServiceFactory class:

com.baeldung.servicemodule.external包中,让我们定义以下TextServiceFactory类。

public class TextServiceFactory {
    
    private TextServiceFactory() {}
    
    public static TextService getTextService(String name) {
        return name.equalsIgnoreCase("lowercase") ? new LowercaseTextService(): new UppercaseTextService();
    }
    
}

Of course, we could have made the factory class slightly more complex. To keep things simple though, the service providers are simply created based on the String value passed to the getTextService() method.

当然,我们可以使工厂类稍微复杂一些。不过为了保持简单,服务提供者只是根据传递给getTextService()方法的String值创建。

Now, let’s replace our module-info.java file to export only our external package:

现在,让我们替换我们的module-info.java文件,只导出我们的external package。

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

Notice that we’re only exporting the service interface and the factory class. The implementations are private, hence they’re not visible to other modules.

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

5.4. The Application Class

5.4.应用类

Now, let’s refactor the Application class, so it can use the service provider factory class:

现在,让我们重构Application类,以便它可以使用服务提供者工厂类。

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

As expected, if we run the application, we should see the same text printed out to the console:

正如预期的那样,如果我们运行该应用程序,我们应该看到在控制台打印出相同的文本。

hello from baeldung!

By making the service interface public and the service providers private effectively allowed us to decouple the service and the consumer modules via a simple factory class.

通过使服务接口公共化和服务提供者私有化,我们可以通过一个简单的工厂类有效地将服务和消费者模块解耦。

No pattern is a silver bullet, of course. As always, we should first analyze our use case for fit.

当然,没有哪个模式是银弹。一如既往,我们应该首先分析我们的用例是否适合。

6. Service and Consumer Modules

6.服务和消费者模块

The JPMS provides support for service and consumer modules out of the box, through the provides…with and uses directives.

JPMS通过 provides…withuses指令,为服务和消费者模块提供开箱即用的支持。

Therefore, we can use this functionality for decoupling modules, without having to create additional factory classes.

因此,我们可以使用这一功能来解耦模块,而不必创建额外的工厂类。

To put service and consumer modules to work together, we need to do the following:

为了让服务和消费者模块一起工作,我们需要做以下工作。

  1. Place the service interface in a module, which exports the interface
  2. Place the service providers in another module – the providers are exported
  3. Specify in the provider’s module descriptor that we want to provide a TextService implementation with the provides…with directive
  4. Place the Application class in its own module – the consumer module
  5. Specify in the consumer module’s module descriptor that the module is a consumer module with the uses directive
  6. Use the Service Loader API in the consumer module to lookup the service providers

This approach is very powerful as it leverages all the functionality that service and consumer modules bring to the table. But it’s somewhat tricky too.

这种方法非常强大,因为它利用了服务和消费者模块带来的所有功能。但它也有些棘手。

On the one hand, we make the consumer modules depend only on the service interface, not on the service providers. On the other hand, we can even not define service providers at all, and the application will still compile.

一方面,我们让消费者模块只依赖于服务接口,而不依赖于服务提供者。另一方面,我们甚至可以完全不定义服务提供者,而应用程序仍然会被编译

6.1. The Parent Module

6.1.父模块

To implement this pattern, we’ll need to refactor the parent POM and the existing modules too.

为了实现这种模式,我们也需要重构父级POM和现有的模块。

Since the service interface, the service providers and the consumer will now live in different modules, we first need to modify the parent POM’s <modules> section, to reflect this new structure:

由于服务接口、服务提供者和消费者现在将生活在不同的模块中,我们首先需要修改父POM的<modules>部分,以反映这种新的结构。

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

6.2. The Service Module

6.2.服务模块

Our TextService interface will go back into com.baeldung.servicemodule.

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

And we’ll change the module descriptor accordingly:

而我们将相应地改变模块描述符。

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

6.3. The Provider Module

6.3.提供者模块

As stated, the provider module is for our implementations, so let’s now place LowerCaseTextService and UppercaseTextService here instead. We’ll put them in a package we’ll call com.baeldung.providermodule.

如前所述,提供者模块是为我们的实现准备的,所以现在让我们把LowerCaseTextService和UppercaseTextService放在这里。我们将把它们放在一个包里,我们称之为com.baeldung.providermodule.

Finally, let’s add a module-info.java file:

最后,让我们添加一个module-info.java文件。

module com.baeldung.providermodule {
    requires com.baeldung.servicemodule;
    provides com.baeldung.servicemodule.TextService with com.baeldung.providermodule.LowercaseTextService;
}

6.4. The Consumer Module

6.4.消费者模块

Now, let’s refactor the consumer module. First, we’ll place Application back into the com.baeldung.consumermodule package.

现在,我们来重构消费者模块。首先,我们将Application放回com.baeldung.consumerermodule包中。

Next, we’ll refactor the Application class’s main() method, so it can use the ServiceLoader class to discover the appropriate implementation:

接下来,我们将重构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!"));
    }
}

Finally, we’ll refactor the module-info.java file:

最后,我们将重构module-info.java文件。

module com.baeldung.consumermodule {
    requires com.baeldung.servicemodule;
    uses com.baeldung.servicemodule.TextService;
}

Now, let’s run the application. As expected, we should see the following text printed out to the console:

现在,让我们运行该应用程序。正如预期的那样,我们应该看到以下文字被打印到控制台。

The service LowercaseTextService says: hello from baeldung!

As we can see, implementing this pattern is slightly more complex than the one that uses a factory class. Even so, the additional effort is highly rewarded with a more flexible, loosely-coupled design.

正如我们所看到的,实现这种模式要比使用工厂类的模式稍微复杂一些。即便如此,额外的努力还是得到了更灵活、更松散的耦合设计的高度回报。

The consumer modules depend on abstractions, and it’s also easy to drop in different service providers at runtime.

消费者模块依赖于抽象,而且在运行时也很容易丢入不同的服务提供者

7. Conclusion

7.结论

In this tutorial, we learned how to implement two patterns for decoupling Java modules.

在本教程中,我们学习了如何实现两个用于解耦Java模块的模式。

Both approaches make the consumer modules depend on abstractions, which is always a desired feature in the design of software components.

这两种方法都使消费者模块依赖于抽象,而这始终是软件组件设计中的一个理想特征。

Of course, each one has its pros and cons. With the first one, we get a nice decoupling, but we have to create an additional factory class.

当然,每一种方法都有其优点和缺点。使用第一种方法,我们得到了一个很好的解耦,但我们必须创建一个额外的工厂类。

With the second one, to get the modules decoupled, we have to create an additional abstraction module and add a new level of indirection with the Service Loader API.

对于第二种情况,为了使模块解耦,我们必须创建一个额外的抽象模块,并通过服务加载器API添加一个新的层次的间接性

As usual, all the examples shown in this tutorial are available on GitHub. Make sure to check out the sample code for both the Service Factory and Provider Module patterns.

像往常一样,本教程中显示的所有示例都可以在GitHub上找到。请务必查看服务工厂供应商模块模式的示例代码。