Java 模块解耦的设计策略

简介: Java 模块解耦的设计策略

Java 平台模块系统 (JPMS) 提供了更强的封装、更高的可靠性和更好的关注点分离,有些同学可能没注意到。

不过呢,也是有利有弊。由于模块化应用程序构建在依赖其他模块才能正常工作的模块网络上,因此在许多情况下,模块彼此紧密耦合。

这可能会让我们认为模块化和松散耦合是不能在同一系统中共存的特性。不过呢,他们可以的!

接着,我们就来深入研究两种众所周知的设计模式,我们可以使用它们轻松解耦 Java 模块。

1、创建项目

我们弄个多模块的 Mavene 项目来演示吧。

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

第一个模块将包括一个服务接口以及两个实现——服务提供者。

第二个模块将使用提供程序来解析字符串值。

咱创建一个名为 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>

2、服务模块

出于演示目的,让我们使用一种快速而肮脏的方法来实现 servicemodule 模块,这样我们就可以清楚地发现此设计中出现的缺陷。

让我们将服务接口和服务提供者公开,将它们放在同一个包中并全部导出。这似乎是一个相当不错的设计选择,但正如我们稍后将看到的,它极大地提高了项目模块之间的耦合程度。

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

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

TextService 接口非常简单,接着我们定义服务提供者。

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

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

现在,让我们添加一个大写实现:

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;
}

3、消费者模块

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

让我们添加 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;
}

最后,运行它。

但有一个值得注意的重要警告:我们不必要地将服务提供者耦合到消费者模块

由于我们使提供者对外界可见,因此消费者模块能够意识到它们。

此外,这不利于使软件组件依赖于抽象。

4、服务提供者工厂

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

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

1、将服务接口放在单独的包中,导出给外界

2、将服务提供者放在不同的包中,该包不导出

3、创建一个工厂类,并将其导出。消费者模块使用工厂类来查找服务提供者

我们可以将上述步骤概念化为设计模式的形式:公共服务接口、私有服务提供者和公共服务提供者工厂。

4.1 公共服务接口

为了清楚地了解此模式的工作原理,让我们将服务接口和服务提供者放在不同的包中。接口将被导出,但提供程序实现不会被导出。

因此,让我们将 TextService 移至一个名为 com.baeldung.servicemodule.external 的新包。

4.2 私服提供者

然后,我们同样将 LowercaseTextService 和 UppercaseTextService 移动到 com.baeldung.servicemodule.internal。

4.3 公共服务提供者工厂

由于服务提供者类现在是私有的,无法从其他模块访问,因此我们将使用公共工厂类来提供一种简单的机制,消费者模块可以使用该机制来获取服务提供者的实例。

在 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() 方法的字符串值创建的。

现在,让我们替换 module-info.java 文件以仅导出外部包:

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

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

4.4 应用类

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

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

然后,运行它。

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

当然,没有任何模式是灵丹妙药。与往常一样,我们应该首先分析我们的用例是否适合。

5、服务和消费者模块

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

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

为了让服务和消费者模块协同工作,我们需要执行以下操作:

1、将服务接口放在模块中,模块导出接口

2、将服务提供者放在另一个模块中 - 提供者被导出

3、在提供者的模块描述符中指定我们想要使用provides…with指令提供TextService实现

4、将 Application 类放置在它自己的模块中——消费者模块

5、在消费者模块的模块描述符中指定该模块是带有使用指令的消费者模块

6、使用消费者模块中的 Service Loader API 来查找服务提供者

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

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

5.1 父模块

为了实现这个模式,我们还需要重构父 POM 和现有模块。

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

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

5.2 服务模块

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

我们将相应地更改模块描述符:

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

5.3 提供者模块

如前所述,提供程序模块用于我们的实现,因此现在让我们将 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;
}

5.4 消费者模块

现在,让我们重构消费者模块。首先,我们将应用程序放回 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;
}

然后,运行它!

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

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

6、总结

我们学习了如何实现两种模式来解耦 Java 模块。

这两种方法都使消费者模块依赖于抽象,这始终是软件组件设计中所需的功能。

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

对于第二个,为了使模块解耦,我们必须创建一个额外的抽象模块并使用 Service Loader API 添加新的间接级别。


目录
相关文章
|
6天前
|
数据采集 存储 Java
高德地图爬虫实践:Java多线程并发处理策略
高德地图爬虫实践:Java多线程并发处理策略
|
22天前
|
缓存 Java
java开发常用模块——缓存模块
java开发常用模块——缓存模块
|
4天前
|
存储 安全 算法
【JAVA】HashMap扩容性能影响及优化策略
【JAVA】HashMap扩容性能影响及优化策略
|
24天前
|
设计模式 监控 Java
设计模式 - 观察者模式(Observer):Java中的战术与策略
【4月更文挑战第7天】观察者模式是构建可维护、可扩展系统的关键,它在Java中通过`Observable`和`Observer`实现对象间一对多的依赖关系,常用于事件处理、数据绑定和同步。该模式支持事件驱动架构、数据同步和实时系统,但需注意避免循环依赖、控制通知粒度,并关注性能和内存泄漏问题。通过明确角色、使用抽象和管理观察者注册,可最大化其效果。
|
3天前
|
Java 测试技术 Android开发
Java 测试和调试:提高代码质量的实用策略
【4月更文挑战第27天】测试和调试是软件开发中确保应用稳定、高效且可靠的关键步骤。对于 Java 开发者来说,掌握有效的测试和调试技巧可以大大提高代码质量和减少生产环境下的问题。
11 2
|
Java 测试技术
Java 中的单元测试和集成测试策略
【4月更文挑战第19天】本文探讨了Java开发中的单元测试和集成测试。单元测试专注于单一类或方法的功能验证,使用测试框架如JUnit,强调独立性、高覆盖率和及时更新测试用例。集成测试则验证模块间交互,通过逐步集成或模拟对象来检测系统整体功能。两者相辅相成,确保软件质量和降低修复成本。
|
14天前
|
Oracle Java 关系型数据库
Java 开发者必备:JDK 版本详解与选择策略(含安装与验证)
Oracle Java SE 支持路线图显示,JDK 8(LTS)支持至2030年,非LTS版本如9-11每6个月发布且支持有限。JDK 11(LTS)支持至2032年,而JDK 17及以上版本现在提供免费商用许可。LTS版本提供长达8年的支持,每2年发布一次。Oracle JDK与OpenJDK有多个社区和公司构建版本,如Adoptium、Amazon Corretto和Azul Zulu,它们在许可证、商业支持和更新方面有所不同。个人选择JDK时,可考虑稳定性、LTS、第三方兼容性和提供商支持。
27 0
|
15天前
|
缓存 NoSQL Java
使用Redis进行Java缓存策略设计
【4月更文挑战第16天】在高并发Java应用中,Redis作为缓存中间件提升性能。本文探讨如何使用Redis设计缓存策略。Redis是开源内存数据结构存储系统,支持多种数据结构。Java中常用Redis客户端有Jedis和Lettuce。缓存设计遵循一致性、失效、雪崩、穿透和预热原则。常见缓存模式包括Cache-Aside、Read-Through、Write-Through和Write-Behind。示例展示了使用Jedis实现Cache-Aside模式。优化策略包括分布式锁、缓存预热、随机过期时间、限流和降级,以应对缓存挑战。
|
17天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
17天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第13天】 在Java并发编程中,锁是一种常见的同步机制,用于保证多个线程之间的数据一致性。然而,不当的锁使用可能导致性能下降,甚至死锁。本文将探讨Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁降级等方法,以提高程序的执行效率。
16 4