隐含马尔可夫 隐含状态
拼图的动手指南超越了我想详细讨论的功能: 隐含可读性 。 有了它,一个模块可以将另一个模块的API重新导出到它自己的依赖项。
总览
这篇文章基于我最近为InfoQ撰写的文章的一部分。 如果您对拼图练习感兴趣,则应阅读整篇文章。
所有非归因报价均来自出色的模块系统状态 。
(隐含的)可读性的定义
一个模块对另一个模块的依赖性可以采取两种形式。
回顾:可读性
首先,有些依赖是内部消耗的,而外界对此一无所知。 在这种情况下,从属模块依赖于另一个模块,但是此关系对于其他模块是不可见的。
以Guava为例,其中依赖于模块的代码根本不在乎内部是否使用不可变列表 。
这是最常见的情况,它由可读性概念涵盖:
当一个模块直接依赖于另一个模块[…]时,第一个模块中的代码将能够引用第二个模块中的类型。 因此,我们说第一模块读取第二模块,或者等效地,第二模块可被第一模块读取 。
在这里,一个模块仅在声明其对另一个模块的依赖关系时才能访问它。 因此,如果模块依赖于Guava,则其他模块就此无所适从,并且在未声明自己对它的显式依赖的情况下将无法访问Guava。
隐含可读性
但是还有另一种用例,其中依赖关系没有完全封装,而是存在于模块之间的边界上。 在这种情况下,一个模块依赖于另一个模块,并在其自己的公共API中公开依赖模块的类型。
在Guava的示例中,模块的公开方法可能期望或返回不可变列表。
因此,要调用依赖模块的代码可能必须使用依赖模块中的类型。 但是,如果它也不读取第二个模块,它就无法做到这一点。 因此,为了使从属模块完全可用,客户端模块也必须全部明确依赖于该第二模块。 识别并手动解决这种隐藏的依赖关系将是一项繁琐且容易出错的任务。
这就是隐含可读性的来源:
[我们]扩展了模块声明,以便一个模块可以将可读性授予该模块依赖的其他模块,以及任何依赖于该模块的模块。 这种隐含的可读性通过在require子句中包含public修饰符来表示。
在使用不可变列表的模块公共API的示例中,该模块将公开要求使用Guava,从而根据其对所有其他模块赋予Guava可读性。 这样,其API即刻可用。
例子
从JDK
让我们看一下java.sql
模块。 它公开了Driver
接口,该接口通过其公共方法getParentLogger()
返回Logger
。 Logger
属于java.logging
。 由于java.sql
公开需要java.logging
,因此任何使用JavaSQL功能的模块都可以访问日志记录API。
因此, java.sql
的模块描述符可能如下所示:
module java.sql {
requires public java.logging;
requires java.xml;
// exports ...
}
从拼图出现日历
日历包含advent.calendar
模块,其中包含24个惊喜列表,每天显示一次。 惊喜是advent.surprise module
一部分。 到目前为止,这看起来像是常规requires
子句的打开和关闭情况。
但是,为了创建日历,我们需要将针对各种意外情况的工厂传递给日历的静态工厂方法 ,该方法是模块公共API的一部分。 因此,我们使用隐含的可读性来确保使用日历的模块将不必显式要求使用Surprise模块。
module org.codefx.demo.advent.calendar {
requires public org.codefx.demo.advent.surprise;
// exports ...
}
超越模块边界
模块系统状态建议何时使用隐式可读性:
通常,如果一个模块导出包含其签名指向第二个模块中的一个包的类型的包,则第一个模块的声明应包括对第二个模块的公共依赖。 这将确保依赖于第一个模块的其他模块将能够自动读取第二个模块,从而访问该模块导出的包中的所有类型。
但是,我们应该走多远?
回顾java.sql
的示例,使用它的模块是否还require java.logging
? 从技术上讲,这样的声明是不需要的,可能看起来是多余的。
为了回答这个问题,我们必须看看我们的虚拟模块到底如何使用java.logging。 它可能只需要阅读即可,因此我们可以调用Driver.getParentLogger()
,更改记录器的日志级别并完成操作。 在这种情况下,我们的代码与java.logging
的交互发生在它与Driver from java.sql
交互的附近。 在上面,我们将此称为两个模块之间的边界。
或者,我们的模块可能实际上在整个自己的代码中使用日志记录。 然后,来自java.logging
类型出现在许多独立于Driver的地方,并且不再被认为仅限于我们模块和java.sql
的边界。
可以为我们的日历创建类似的并置:是否需要advent.calendar
的主模块advent
仅将advent.surprise
用于创建日历所需的惊喜工厂? 还是它独立于与日历的交互而用于惊喜模块?
如果一个模块不仅仅用于另一个模块的边界,则应该明确要求该模块。
随着拼图技术的发展,社区仍然有时间讨论这些主题并就推荐的做法达成共识。 我的观点是,如果一个模块不仅在另一个模块的边界上使用,还应该明确要求它。 这种方法阐明了系统的结构,并在将来对各种重构的模块声明进行了验证。
聚集与分解
隐含的可读性启用了一些有趣的技术。 他们依靠这样的事实,即客户端可以使用各种模块的API,而不必显式地依赖于它们,如果客户端依赖于公开需要使用的模块的话。
聚合器模块将相关模块的功能捆绑到一个单元中。
一种技术是创建所谓的聚合器模块 ,这些聚合器模块自身不包含任何代码,但是聚合了许多其他API以便于使用。 Jigsaw JDK已经采用了这种方法,该模型将紧凑的概要文件建模为模块,这些模块简单地公开了包是概要文件一部分的模块。
另一个是Alex Buckley所谓的向下可分解性 :如果一个模块成为新模块的聚集器,则可以将其分解为更专业的模块,而不会产生兼容性问题。
但是,创建聚合器模块会使客户陷入一种情况,即客户在内部使用其未明确依赖的模块的API。 可以认为这与我们上面所说的相冲突,即隐含的可读性仅应在其他模块的边界上使用。 但是我认为这里的情况略有不同。
聚合器模块负有特定责任:将相关模块的功能捆绑到一个单元中。 修改包的内容是一项关键性的更改。 另一方面,“常规”隐含的可读性通常会在不直接相关的模块之间出现(如java.sql
和java.logging
),在这些模块之间偶然使用隐含模块。
这有点类似于组合和聚合之间的区别,但是(a)不同,并且(b)可悲的是,聚合器模块更多地位于组合方面。 我很高兴听到有关如何精确表达差异的想法。
反射
我们已经看到了如何使用隐式可读性使模块的公共API立即可用,即使该模块包含另一个模块的类型也是如此。 它启用了聚合器模块并具有向下的可分解性。
我们讨论了应将隐含可读性扩展到什么程度,并且我认为,如果模块仅在隐式依赖的模块边界上使用隐含模块的API,则它仅应依赖于隐含可读性。 这不会涉及聚合器模块,因为它们将机制用于其他目的。
翻译自: https://www.javacodegeeks.com/2016/02/implied-readability.html
隐含马尔可夫 隐含状态