方法链的优缺点及将所有void返回参数替换为对象本身的可能性

25

我主要对Java感兴趣,但我认为这是一个普遍的问题。最近我一直在使用Arquillian框架(ShrinkWrap),该框架使用了大量的方法链式调用。StringBuilder、StringBuffer等类中也有方法链式调用的例子。使用此方法的明显优点之一是减少冗余。

现在我想知道,为什么不将所有返回void参数的方法都实现为链式调用呢?肯定存在一些明显而客观的缺点。因为如果所有方法都可链接,我仍然可以选择不使用它。

我不是要求改变Java中现有的代码,在某个地方会出现问题,但从未来Java框架设计的角度考虑,解释为什么没有使用链式调用也很好。


我找到了一个类似的问题,但原问题的提问者实际上正在思考为什么链式调用被认为是一个好的实践: Method chaining - why is it a good practice, or not?


虽然已经有一些答案可供参考,但我仍然不确定链式调用的所有优缺点以及是否将所有无返回值方法都实现为链式调用是否有价值。

9个回答

27

缺点

  • 主要是会混淆签名,如果某个方法返回一个新实例,我不希望它也是一个修改器方法。例如,如果向量有一个比例方法,则如果它具有返回值,我会认为它返回一个按输入比例缩放的新向量,如果没有,我会期望它在内部进行缩放。
  • 当类被扩展时,还会出现问题,部分通过链接对象在其过程中被强制转换为超类型。这发生在父类中声明连接方法,但在子类实例上使用时。

优点

  • 它允许编写以数学方程式为风格的代码作为完整方程式,而无需多个中间对象(导致不必要的开销),例如,如果没有方法链接,则向量三重叉积(作为随机示例)必须写成

    MyVector3d tripleCrossProduct=(vector1.multiply(vector2)).multiply(vector3);
    

    有一个缺点,就是会创建一个中间对象,需要被创建和垃圾回收。

    MyVector3d tripleCrossProduct=vector1;
    tripleCrossProduct.multiplyLocal(vec2);
    tripleCrossProduct.multiplyLocal(vec3);
    

    这种方法避免了中间对象的创建,但是却非常不清晰,变量名tripleCrossProduct直到第3行才被真正实现。但是,如果你使用方法链,就可以用一种正常的数学方式简洁地写出来,而不会创建不必要的中间对象。

    MyVector3d tripleCrossProduct=vector1.multiplyLocal(vector2).multiplyLocal(vector3);
    

    所有这些都假设vector1是牺牲的,并且永远不需要再次使用

  • 当然,明显的好处是简洁性。即使您的操作没有像我上面的示例那样链接,您仍然可以避免不必要地引用对象

  • SomeObject someObject=new SomeObject();
    someObject
      .someOperation()
      .someOtherOperation();
    

请注意,Java中的MyVector3d并非真正的类,但假定在调用.multiply()方法时会执行叉乘操作。没有使用.cross()方法,以使那些不熟悉向量微积分的人更容易理解“意图”
请注意,Amit的解决方案是第一个使用多行方法链接的答案,我将其作为完整性的一部分包含在第四个要点中


+1,指出了两个问题,但我不同意可读性问题,因为您仍然可以像Amit的示例中那样添加换行符。 - user829755
@user829755 你可能是对的,我把“允许你做坏事”等同于“导致你做坏事”。 - Richard Tingle
+1 谢谢,现在我明白你在另一个答案中评论的要点了。 - MartinTeeVarga
我已经将您答案中的Vector更改为Vector2d,以避免混淆,并将其从脚注中删除。在Java中,Vector2d通常用于这样的类,我希望大多数人都能理解它。请在接受编辑之前考虑一下。 - MartinTeeVarga
@sm4 感谢您,sm4。您的编辑似乎在我来得及查看之前就已经完成了。然而,我故意选择了一个虚构的类,这样我就可以给它同时具有非本地和本地方法,以及可链接和不可链接版本。真正的Vector3d类并不具备这三个特点,所以我不想让人们陷入我的代码片段无法工作的困境中。 - Richard Tingle
显示剩余2条评论

26

方法链是一种实现流畅接口的方式,无论编程语言如何。它的主要优点(可读性代码)告诉您何时使用它。如果没有特别需要可读性的代码,则最好避免使用它,除非API自然设计为返回上下文/对象作为方法调用的结果。

步骤1:流畅接口 vs. 命令查询API

必须将流畅接口与命令查询API进行比较。为了更好地理解它,让我写出命令查询API的项目列表定义。简单来说,这只是标准的面向对象编码方法:

  • 修改数据的方法称为Command。命令不返回值。
  • 返回值的方法称为Query。查询不修改数据。

遵循命令查询API将带来以下好处:

  • 通过查看面向对象的代码,您可以了解正在发生什么。
  • 调试代码更容易,因为每个调用都是独立的。

步骤2:在命令查询API之上实现流畅接口

但命令查询API存在某些原因,并且确实更易读。那么我们如何同时拥有流畅接口和命令查询API的好处呢?

答案:必须在命令查询API之上实现流畅接口(而不是通过使用流畅接口替换命令查询API)。将流畅接口视为命令查询API的外观即可。毕竟,它被称为流畅“接口” - 标准(命令查询)API的可读性或方便性接口。

通常,在命令查询API准备好之后(编写,可能进行单元测试,精细调试),您可以在其上编写一个流畅接口软件层。换句话说,流畅接口通过利用命令查询API来实现其功能。然后,在需要方便和可读性的任何地方使用流畅接口(通过方法链接)。但是,一旦您想了解到底发生了什么(例如调试异常),您可以随时深入研究命令查询API-良好的面向对象代码。


4
这听起来像是一个很不错的框架设计想法。我希望你能得到一些赞同,这样我就不会是唯一一个喜欢它的人了。 - MartinTeeVarga

8
我发现使用方法链接的缺点是在调试代码时出现NullPointerException或其他Exception时很难。假设你有以下代码: String test = "TestMethodChain"; test.substring(0,10).charAt(11); //这只是一个例子 当执行上面的代码时,您将得到“String index out of range”异常。在实时情况下遇到这种情况时,可以知道哪一行出错了,但不知道是链接方法的哪一部分引起了错误。因此,需要明智地使用它,其中已知数据始终会出现或错误已经正确处理。
它也有其优点,如您无需编写多行代码和使用多个变量。
许多框架/工具都使用它,例如Dozer,当我调试代码时,我必须查看链的每个部分以查找导致错误的原因。
希望这有所帮助。

感谢马丁的改进。 - Harish Kumar
6
通常,您可以告诉调试器在异常发生时停止,并显示堆栈跟踪信息。当我在Intellij IDEA中对您的示例进行调试时,它会直接跳转到String.charAt(11)处。 - mbatchkarov
@mbatchkarov 这在开发过程中使用IDE时是有效的,但在生产代码中则不行。 - Donut

6

您可能想阅读Martin Fowler的流畅接口(Fluent Interface)

总结

  • 不要仅仅为了链式调用而链式调用,因为这会破坏命令查询职责分离(CQRS)设计原则。
  • 通过将API设计与业务操作方式更加贴近,将其视为内部DSL,来改善API设计。
  • 尽量避免链接独立方法,因为这会污染API并可能无法向客户端/代码维护人员显示意图。

4

这种模式在需要对同一对象进行一系列更新操作,并且更新操作不需要返回任何更新状态时非常有用。例如,我在为数据库层编写的一些API中使用了这种模式。为了根据许多条件获取一些行,需要将许多条件添加到WHERE子句中。使用这种模式,可以按如下方式添加条件。

CriteriaCollection().instance()
    .addSelect(Criteria.equalTo(XyzCriteria.COLUMN_1, value1))
    .addSelect(Criteria.equalTo(XyzCriteria.COLUMN_2, value2))
    .addSelect(Criteria.isIn(XyzCriteria.COLUMN_3, values3))
    .addOrder(OrderCriteria.desc(XyzCriteria.Order.COLUMN_1));

它最终提高了代码的可读性。


2

如果您喜欢 不可变性函数式编程,您永远不会返回 void

没有返回值的函数仅用于其 副作用

当然,有时这种情况并不适用,但是返回 void 的函数可以被视为尝试以不同方式解决问题的提示。


虽然这是一个有趣的想法,但返回对象本身以进行方法链接并不会改变它的任何内容。它仍然只会因为其副作用而被调用。 - martinstoeckli
@martinstoeckli 你说得对 - 我在不可变性/函数式编程的背景下加入了这一点,只是为了从不同的角度来看待这个问题。 - Beryllium

1
个人认为这是一个非常有用的模式,但不应该随处使用。考虑一下你有一个 copyTo(T other) 方法的情况。通常你不希望它返回任何东西,但如果它返回一个相同类型的对象,你会期望哪个对象呢?这种问题可以通过文档来澄清,但方法签名仍然存在歧义。
public class MyObject {

    // ... my data

    public MyObject copyTo(MyObject other) {

        //... copy data


        // what should I return?
        return this;
    }
}

1
这是一项相当大的工作,尤其是涉及到继承时。可以看看关于建造者模式的这篇优秀文章
当您预计客户端将按顺序调用同一实例上的多个方法(例如建造者模式、不可变对象等)时,实现它是有意义的。但在我看来,在大多数情况下,这并不值得额外的努力。

在编写框架时,大量的工作并不是一个好的论据。如果能使您的框架更好、更易于使用、更健壮和稳定,尽可能多地投入工作是明智的选择。 - MartinTeeVarga
2
@sm4 如果有人扩展了你的框架类,那么你已经把这项工作交给了他们。 - Richard Tingle

0

对象具有属性和方法。每个方法都实现了对象的整体目的的一部分。某些类型的方法,如构造函数和获取器和设置器,执行属性和对象本身的生命周期管理。其他方法返回对象及其属性的状态。这些方法通常是非空的。

空方法有两种类型: 1. 关于对象或属性的生命周期管理。 2. 具有在方法内完全处理的输出,并且不应在任何其他地方引起任何状态更改。

广告1.它们属于对象的内部工作。 广告2.参数的信息用于在方法内执行一些工作。方法完成后,对象本身或其中一个参数的内部状态都没有发生变化。

但是为什么要使方法返回void,而不是例如布尔值(成功或失败)?方法返回void(如setter中)的动机是该方法旨在没有副作用。返回true或false可能是副作用。当按设计执行方法时,void意味着该方法没有副作用。返回异常的void方法很好,因为异常不是方法正常处理的副作用。

返回void的方法意味着它本质上是一个孤立的工作方法。一系列void方法是一系列松散耦合的方法,因为没有其他方法可以依赖其前任的结果,因为没有任何方法有任何结果。对此有一个设计模式,即责任链设计模式。链接不同的记录器是一个传统的例子,以及在servlet api中调用后续过滤器。

以有意义的方式链接void方法意味着这些方法所工作的共享对象在每个步骤之后处于可理解的状态,该状态由工作于该对象的void方法确定。所有后续调用都不能依赖其前任的结果,也不能影响其自身调用后的调用工作。您可以确保最简单的方法是不让它们更改对象的内部状态(例如记录器示例),或者让每个方法更改对象内部状态的另一部分。

在我看来,可以链接的无返回值方法只有那些具有第二种特性并共享某种类型处理的方法。对于涉及类生命周期的无返回值方法,我不会将它们链接起来,因为每个方法都是对象整体状态的不同部分的更改。这些方法的存在可能随着类设计的任何更改而发生变化,因此我不建议将这些方法链接起来。此外,由第一种类型的无返回值方法抛出的所有异常彼此独立,而您可能期望由共享某种类型处理的第二种风格的链接无返回值方法抛出的异常具有某些共同点。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接