解耦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 的定义中有一些值得强调的细节。
首先,该文件包含我们上面提到的两个子模块,即 servicemodule 和 comsumermodule(我们稍后详细讨论它们)。
然后,由于我们使用 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工厂。
香港理工大学 香港PolyU香港科技大学 香港HKUST香港教育学院 香港EdUHK香港岭南大学 香港LU澳门科技大学 香港MUST澳门理工学院 澳门IPM香港浸会大学 香港HKBU澳门城市大学 澳门CUM香港城市大学 香港CityU澳门旅游学院 澳门IFT共和理工学院 新加坡RP南洋艺术学院 新加坡NAFA义安理工学院 新加坡NP淡马锡理工学院 新加坡TP科廷大学 新加坡Curtin 南洋理工学院 新加坡ntu 英迪大学 马来INTI 世纪大学 马来SEGi 亚太科技大学 马来APU 精英大学 马来HELP 汉阳大学 韩国Hanyang 庆熙大学 韩国Kyung 东国大学 韩国Dongguk 高丽大学 韩国Korea 建国大学 韩国Konkuk *大学 韩国Chung 延世大学 韩国Yonsei 成均馆大学 韩国SKKU 弘益大学 韩国Hongik 梨花女子大学 韩国EWU早稻田大学 日本Waseda筑波大学 日本Tsukuba立命馆大学 日本Ritsumeikan东京艺术大学 日本Tokyo名古屋大学 日本Nagoya九州大学 日本Kyushu美国大学 美国大学 美国大学 美国大学 美国大学 美国大学 美国大学 美国大学 美国大学 美国大学 美国大学 美国大学 美国大学 美国大学 美国大学
5.1. 公共的 Service 接口
要清楚的知道该模式如何运作,让我们将 service 接口和 service provider放到不同的包中。接口将被导出,但provider实现不会被导出。
因此,将 TextService 移到叫做 com.baeldung.servicemodule.external 的新包。
5.2. 私有的 Service provider
然后,类似的将 LowercaseTextService 和 UppercaseTextService 移动到 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…with 和 uses 指令为 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>
爱丁堡大学 英国Edinburgh伯明翰大学 英国UoB杜伦大学 英国Durham拉夫堡大学 英国LU华威大学 英国Warwick巴斯大学 英国Bath白金汉大学 英国UCB牛津大学 英国Oxon帝国理工学院 英国IC伯明翰大学 英国UoB格拉斯哥大学 英国Glasgow南安普顿大学 英国Soton诺丁汉大学 英国UNUK萨里大学 英国Surrey*兰开夏大学 英国UCLAN牛津布鲁克斯大学 英国UNUK伯明翰城市大学 英国BCU西英格兰大学 英国UWE阿伯丁大学 英国Aberdeen萨塞克斯大学 英国Sussex伦敦政治经济学院 英国LSE约克大学 英国York伦敦大学学院 英国London谢菲尔德大学 英国Sheffield埃克塞特大学 英国Exon伦敦艺术大学 英国UAL金斯顿大学 英国Kingston伦敦玛丽女王大学 英国QMUL布鲁内尔大学 英国Brunel伦敦城市大学 英国CITY伯恩茅斯大学 英国BU朴次茅斯大学 英国UOP卡迪夫城市大学 英国UWIC赫特福德大学 英国Hertford爱丁堡艺术学院 英国ECA澳门大学 澳门UM香港中文大学 香港CUHK
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 Factory和 Provider Module 模式的示例代码。
-多伦多大学 Toronto滑铁卢大学 Waterloo英属哥伦比亚大学 UBC阿尔伯塔大学 UA西安大略大学 UWO-不列颠哥伦比亚大学 UBC-曼尼托巴大学 UM-西蒙弗雷泽大学 Clan麦吉尔大学 McGill维多利亚大学 UVic西蒙菲莎大学 SFU布鲁克大学 Brock萨省大学 Saskatchewan纽芬兰纪念大学 Newfoundland渥太华大学 Ottawa温莎大学 Windsor温莎大学 Windsor卡尔顿大学 Carleton卡普顿大学 CBU卡尔加里大学 UofC圭尔夫大学 Guelph肯考迪亚大学 Concordia温哥华岛大学 VIU百年理工学院 CENTENNIAL戴尔豪斯大学 Dalhousie昆特兰理工大学 KPU西三一大学 TWU卡普兰诺大学 Capilano亚岗昆学院 Algonquin劳里埃大学 WLU乔治布朗学院 George Brown道格拉斯学院 Douglas哥伦比亚国际学院 CIC昆士兰大学 澳洲UQ莫纳什大学 澳洲Monash新南威尔士大学 澳洲UNSW澳洲国立大学 澳洲ANU麦考瑞大学 澳洲Macquarie阿德雷德大学 澳洲Adelaide墨尔本大学 澳洲Unimelb皇家墨尔本理工大学 澳洲RMIT悉尼科技大学 澳洲UTS卧龙岗大学 澳洲UOW南澳大学 澳洲UniSA迪肯大学 澳洲Deakin塔斯马尼亚大学 澳洲UTAS悉尼大学 澳洲USYD澳洲纽卡斯尔大学 澳洲UON格里菲斯大学 澳洲GU昆士兰科技大学 澳洲QUT*昆士兰大学 澳洲CQU维多利亚大学 澳洲UVic詹姆斯库克大学 澳洲JCU堪培拉大学 澳洲UC南十字星大学 澳洲Southern Cross西悉尼大学 澳洲WSU斯威本科技大学 澳洲SUT南昆士兰大学 澳洲USQ新英格兰大学 澳洲England莫道克大学 澳洲MU拉筹伯大学 澳洲LTU西澳大学 澳洲UWA邦德大学 澳洲Bond阳光海岸大学 澳洲USC曼彻斯特大学 英国UoM雷丁大学 英国UoR亚伯大学 英国Aber卡迪夫大学 英国Cardiff利兹大学 英国Leeds利物浦大学 英国Liverpool考文垂大学 英国Coventry