1. An overview of the

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

But all this convenience comes at a price. Because modular applications are built on a web of modules that depend on other functioning modules, in many cases modules are tightly coupled to each other.

This may lead us to think that modularity and loose coupling are characteristics that cannot coexist in the same system. But it can!

In this tutorial, we’ll delve into two well-known design patterns that we can use to easily decouple Java modules.

2. The parent module

To demonstrate the design patterns used to decouple Java modules, we will build a demo of a multi-module Maven project.

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

The first module will contain a service interface and two implementations — service Providers. The second module will use this provider to parse the String value.

Let’s define the project’s parent POM by creating the project root directory called DemoProject:

<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>
Copy the code

There are some details worth highlighting in the definition of the parent POM.

First, this file contains the two submodules we mentioned above, servicemodule and ComSumerModule (we’ll discuss them in more detail later).

Then, since we are using Java 11, our system needs at least Maven 3.5.0, since Maven supports Java 9 and later from that release.

Finally, we need a minimum 3.8.0 version of the Maven compilation plug-in. Therefore, in order to ensure we are up to date, check the [Maven Central] (search.maven.org/classic/#se… AND a%3A”maven-compiler-plugin”) to get the latest version of the Maven compiler plug-in.

3. The Service module

For demonstration purposes, we implement the Servicemodule module in a quick and easy way so that we can clearly see the pitfalls of this design.

Let’s expose the Service interface and the Service Provider, put them in the same package and export all these interfaces. This seems like a pretty good design choice, but as we’ll see later, it greatly increases the coupling between the modules of the project.

In the root directory of the project, we create servicemodule/SRC/main/Java directory. Then, in the defined package com. Baeldung. Servicemodule, and placed in it the following TextService interface:

public interface TextService {

    String processText(String text);

}
Copy the code

The TextService interface is very simple; now let’s define the service provider. Under the same package, add a Lowercase implementation:

public class LowercaseTextService implements TextService {

    @Override
    public String processText(String text) {
        returntext.toLowerCase(); }}Copy the code

Now, let’s add an Uppercase implementation:

public class UppercaseTextService implements TextService {

    @Override
    public String processText(String text) {
        returntext.toUpperCase(); }}Copy the code

Finally, in the servicemodule/SRC/main/Java directory, let us introduce module, the module – info. Java:

module com.baeldung.servicemodule {
    exports com.baeldung.servicemodule;
}
Copy the code

4. Consumer module

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

Let’s add the following com. Baeldung. Consumermodule. Application class:

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

Now, module description, introduced in the root directory of the source code, the module – info. Java, should be in consumermodule/SRC/main/Java:

module com.baeldung.consumermodule {
    requires com.baeldung.servicemodule;
}
Copy the code

Finally, compile the source file from the IDE or command console and run the application.

As expected, we should see the following output:

hello from baeldung!
Copy the code

This works, but with one important caveat: we don’t have to couple the Service Provider and Consumer modules.

Since we made the provider visible to the outside world, the Consumer module will know about them.

Furthermore, this conflicts with software components’ reliance on abstraction.

5. Service Provider factory

We can easily remove the coupling between modules by exposing only the Service interface. In contrast, the Service Provider is not exported, so the Consumer module is kept hidden. The Consumer module only sees the Service interface type.

To achieve this, we need to:

  1. Place the Service interface into a separate package that will be exported to the outside world
  2. Place the Service Provider in a package that will not be exported
  3. Create the exported factory class. The Consumer module uses the factory class to find the Service provider

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

5.1. Common Service interfaces

To see how this pattern works, let’s put the Service interface and the Service Provider in separate packages. The interface is exported, but the Provider implementation is not.

Therefore, moving TextService to called com. Baeldung. Servicemodule. External new package.

5.2. Private Service Providers

Then, a similar move LowercaseTextService and UppercaseTextService to com. The baeldung. Servicemodule. Internal.

5.3. Common Service Provider factories

Since the Service Provider class is now private and not accessible from other modules, we will use the public factory class to provide a simple mechanism that the consumer module can use to get service Provider instances.

In com. Baeldung. Servicemodule. External package, define the following TextServiceFactory class:

public class TextServiceFactory {

    private TextServiceFactory(a) {}

    public static TextService getTextService(String name) {
        return name.equalsIgnoreCase("lowercase")?new LowercaseTextService(): newUppercaseTextService(); }}Copy the code

Of course, we could make the factory class a little more complicated. For simplicity, simply create the Service Provider from the String value passed to the getTextService() method.

Now, place the module-info.java file only to export the external package:

module com.baeldung.servicemodule {
    exports com.baeldung.servicemodule.external;
}
Copy the code

Note that we only exported the Service interface and factory class. Implementations are private, so they are not visible to other modules.

5.4. The Application class

Now, let’s refactor the Application class so that it can use the Service Provider factory class:

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

As expected, if we run the application, we can wire the same text to the console:

hello from baeldung!
Copy the code

By making the Service interface public and the Service Provider private, this effectively allows us to decouple the Service and Consumer modules through simple factory classes.

Of course, no model is a silver bullet. As always, we should first analyze the situation for which we fit.

6. Service and Consumer modules

JPMS through provides… The With and USES directives provide out-of-the-box support for the Service and Consumer modules.

Therefore, we can use this feature to decouple modules without creating additional factory classes.

To make the Service and Consumer modules work together, we need to do the following:

  1. Put the Service interface into the module that exports the interface
  2. Place the Service Provider in another module — the provider is exported
  3. Used in the module description of the providerProvides… withThe directive specifies what we are going to useTextServiceimplementation
  4. willApplicationThe class is placed in its own module, the Consumer module
  5. Used in the consumer module descriptionusesThe directive specifies that this module is the consumer module
  6. Use the Service Loader API in the Consumer module to find the Service Provider

This approach is powerful because it leverages all the functionality that comes with the Service and Consumer modules. But this is a bit tricky.

On the one hand, we made the Consumer module rely only on the Service interface, not the Service Provider. On the other hand, we can’t even define a Service applicator at all, but the application can still compile.

6.1. The parent module

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

Since the Service interface, Service Provider, and consumer will exist in different modules, we first modify the part of the parent POM to reflect the new structure:

<modules>
    <module>servicemodule</module>
    <module>providermodule</module>
    <module>consumermodule</module>
</modules>
Copy the code

6.2. The Service module

TextService interface will return to the com. Baeldung. Servicemodule.

We will change the module description accordingly:

module com.baeldung.servicemodule {
    exports com.baeldung.servicemodule;
}
Copy the code

6.3. The Provider module

As mentioned above, the provider module is our implementation, so let’s now place LowerCaseTextService and UppercaseTextService here. Place them into what we call com. Baeldung. Providermodule package.

Finally, add the module-info.java file:

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

6.4 Consumer module

Now, refactor the Consumer module. First, put your Application into the com. Baeldung. Consumermodule package.

Next, refactor the Main () method of the Application class so that it can use the ServiceLoader class to discover the appropriate implementation:

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!")); }}Copy the code

Finally, rebuild the module-info.java file:

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

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

The service LowercaseTextService says: hello from baeldung!
Copy the code

As you can see, implementing this pattern is a little more complicated than using the factory class. Even so, extra effort results in a more flexible, loosely-coupled design.

The Consumer module relies on abstraction and can easily switch between different service providers at run time.

7. To summarize

In this tutorial, you learned how to decouple the two modes of Java modules.

Both approaches make the Consumer module rely on abstraction, which is always expected in software component design.

Of course, each has its advantages and disadvantages. For the first, we get good decoupling, but we have to create additional factory classes.

For the second, to understand the decoupled modules, we had to create additional abstract modules and add a new middle layer using the Service Loader API.

As always, all of the examples shown in this tutorial are available on GitHub. Be sure to see sample code for the Service Factory and Provider Module patterns.

Original link: www.baeldung.com/java-module…

By Alejandro Ugarte

Translator: Darren Luo