Streams vs. Decorators

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

Streams API是在Java 8中引入的,与lambda表达式一起,仅仅几年前。作为一个训练有素的Java专家,我尝试在我的几个项目中使用这个新特性,例如这里这里。我并不真的喜欢它,而是回到了那些老式的修饰器。此外,我创建了Cactoos,一个修饰器库,用来替代在很多地方都表现不好的Guava

这里是一个简单的例子。假设我们有一个来自某个数据源的测量值集合,它们都是介于零和一之间的数字:

现在,我们需要仅显示其中的前10个,忽略零和一,并将它们重新缩放为(0..100)。听起来很简单,对吧?有三种方法可以做到:过程化、面向对象和Java 8方式。让我们从过程化方式开始:

这为什么是一种过程化的方式呢?因为它是命令式的。为什么它是命令式的呢?因为它是过程化的。呵呵,开个玩笑。

它是命令式的,因为我们正在向计算机发出指令,告诉它将数据放在哪里以及如何迭代遍历。我们并没有声明结果,而是命令式地构建它。这样做是有效的,但不太可扩展。我们无法将这个算法的一部分应用于另一个用例。我们无法轻松地修改它,例如从两个来源获取数字而不是一个等等。这是过程化的。说到此为止。不要以这种方式进行。

现在,Java 8为我们提供了流 API,据说可以以函数式的方式进行相同的操作。让我们试着使用它。

首先,我们需要创建一个Stream的实例,而Iterable不能直接获取它。然后我们使用流 API 来完成任务:

这将起作用,但对于所有探测器都会显示Probe #0,因为forEach()不能使用索引。截至Java 8(以及Java 9 也是如此),在Stream接口中没有forEachWithIndex()这样的方法。这里有一个使用原子计数器的解决方法

“那有什么问题吗?”你可能会问。首先,看看当我们在Stream接口中没有找到正确的方法时,我们是多么容易陷入麻烦。我们立即脱离了”流式”范式,回到了古老的过程化全局变量(计数器)。其次,我们并不真正知道filter()limit()forEach()方法内部发生了什么。它们究竟是如何工作的?文档说这种方法是”声明式”的,Stream接口中的每个方法都返回某个类的实例。它们是哪些类呢?仅凭这段代码我们就一无所知。

这两个问题是相互关联的。这个流式API最大的问题是Stream这个接口本身——它太庞大了。在撰写本文时,它有43个方法。43个方法,就在一个接口中!这与面向对象编程的每一个原则相悖,从SOLID开始,一直到更严肃的原则。

那么,用面向对象的方式来实现相同的算法该怎么做呢?下面是我使用Cactoos来实现的方式,它只是一组简单的Java类:

让我们看看这里发生了什么。首先,Filtered修饰我们的可迭代对象probes,从中取出特定的项。请注意,Filtered实现了Iterable。然后Limited,也是一个Iterable,只取出前十个项。然后Mapped将每个探测器转换为Scalar<Boolean>的实例,它执行行打印操作。

最后,And的实例遍历“标量”列表,并要求每个标量返回boolean值。它们打印行并返回true。由于是trueAnd会尝试下一个标量。最后,它的value()方法返回true

但是等等,这里没有索引。让我们添加它们。为了做到这一点,我们只需使用另一个名为AndWithIndex的类:

不再使用Scalar<Boolean>,我们现在将我们的探测器映射到Func<Integer, Boolean>,以便它们接受索引。

这种方法的优点是所有的类和接口都很小,所以它们非常可组合。为了限制探测器的可迭代性,我们使用Limited进行装饰;为了对其进行过滤,我们使用Filtered进行装饰;为了做其他操作,我们创建一个新的装饰器并使用它。我们不受限于像Stream这样的单一接口。

底线是,装饰器是一种面向对象的工具,用于修改集合的行为,而流是另一种我甚至找不到名字的东西。

顺便说一下,这是如何使用Guava的Iterables来实现相同算法的方式:

这是一种奇怪的面向对象和函数式风格的组合。

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-27 at 10:44

sixnines availability badge   GitHub stars