1. 概述

Java 平台模块系统(JPMS)带来了更强的封装性、更高的可靠性以及更清晰的职责划分。但这些特性也带来了模块间耦合度变高的问题。

很多开发者会误以为“模块化”与“松耦合”无法共存。其实不然,本文将介绍两种设计模式,帮助我们在模块化 Java 应用中实现模块间的松耦合。

2. 父项目结构

我们以一个多模块 Maven 项目为例进行讲解。该项目包含三个模块:

  • servicemodule:定义服务接口
  • providermodule:实现服务接口
  • consumermodule:使用服务接口

父 POM 的配置如下:

<packaging>pom</packaging>

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

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.12.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

📌 注意:

  • 项目使用 Java 11,需要 Maven 3.5.0 以上版本
  • 推荐使用最新版的 Maven Compiler Plugin(至少 3.8.0)

3. 服务模块

我们先定义一个简单的服务接口和两个实现类:

// TextService.java
public interface TextService {
    String processText(String text);
}
// LowercaseTextService.java
public class LowercaseTextService implements TextService {
    @Override
    public String processText(String text) {
        return text.toLowerCase();
    }
}
// UppercaseTextService.java
public class UppercaseTextService implements TextService {
    @Override
    public String processText(String text) {
        return text.toUpperCase();
    }
}

模块描述文件:

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

⚠️ 问题:此时服务实现类是对外暴露的,导致消费者模块直接依赖具体实现,不符合“依赖抽象”的设计原则。

4. 消费者模块

消费者模块直接使用具体的服务实现类:

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

模块描述文件:

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

输出结果为:

hello from baeldung!

✅ 功能正常,但 ❌ 存在紧耦合:消费者模块直接依赖服务实现类,不利于扩展和维护。

5. 服务提供工厂模式

我们通过工厂类隐藏服务实现细节,只暴露接口和工厂类,从而实现模块解耦。

5.1 公共服务接口

将接口移到独立包中并导出:

package com.baeldung.servicemodule.external;

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

5.2 私有服务实现

实现类移至内部包,不导出:

package com.baeldung.servicemodule.internal;

public class LowercaseTextService implements TextService {
    @Override
    public String processText(String text) {
        return text.toLowerCase();
    }
}
package com.baeldung.servicemodule.internal;

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

5.3 公共工厂类

package com.baeldung.servicemodule.external;

public class TextServiceFactory {
    private TextServiceFactory() {}

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

模块描述文件仅导出接口和工厂类:

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

5.4 消费者模块重构

使用工厂类获取服务实例:

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

输出结果:

hello from baeldung!

✅ 优点:

  • 消费者模块只依赖接口和工厂类
  • 实现类对消费者不可见,实现了解耦

❌ 缺点:

  • 需要手动编写工厂类
  • 工厂类逻辑可能变得复杂

6. 服务提供者和服务消费者模块

Java 原生支持服务提供者和服务消费者模块机制,通过 provides...withuses 指令实现模块解耦。

6.1 项目结构调整

父 POM 更新为:

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

6.2 服务模块

仅导出接口:

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

6.3 提供者模块

实现接口并注册为服务提供者:

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

6.4 消费者模块

使用 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.processText("Hello from Baeldung!"));
    }
}

模块描述文件:

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

输出结果:

The service LowercaseTextService says: hello from baeldung!

✅ 优点:

  • 无需手动编写工厂类
  • 支持运行时动态加载服务实现
  • 更符合“依赖抽象”的设计原则

❌ 缺点:

  • 配置较复杂
  • 需要额外模块管理服务实现

7. 总结

我们介绍了两种实现 Java 模块解耦的设计方式:

方式 优点 缺点
服务工厂模式 实现简单,控制灵活 需要手动编写工厂类
服务提供者/消费者模块 原生支持,高度解耦,支持运行时插拔 配置复杂,学习成本高

📌 建议:

  • 对于小型项目或快速原型开发,推荐使用服务工厂模式
  • 对于大型模块化系统,尤其是需要插件化架构的项目,推荐使用服务提供者/消费者模块机制

所有示例代码已上传至 GitHub,欢迎参考:GitHub 项目地址


原始标题:Design Strategies for Decoupling Java Modules