24

我对 Java 最感兴趣,但我认为这是一个普遍的问题。最近我一直在使用 Arquillian 框架(ShrinkWrap),它使用了很多方法链。方法链接的其他示例是 , 中的StringBuilder方法StringBuffer。使用这种方法有明显的好处:减少冗长就是其中之一。

现在我想知道,为什么不是所有具有void返回参数的方法都实现为可链接的?链接必须有一些明显和客观的缺点。因为如果所有方法都是可链接的,我仍然可以选择不使用它。

我不是要求更改 Java 中的现有代码,这可能会在某处破坏某些东西,但解释为什么不使用它也会很好。我更多地从未来框架(用Java编写)设计的角度提出问题。


我发现了一个类似的问题,但最初的提问者实际上想知道为什么它被认为是一种好的做法:方法链 - 为什么它是一种好的做法,或者不是?


虽然有一些可用的答案,但我仍然不确定链接的所有优点和缺点是什么,以及将所有 void 方法链接起来是否有用。

4

9 回答 9

26

缺点

  • 主要是它混淆了签名,如果某些东西返回一个新实例,我不希望它也是一个 mutator 方法。例如,如果一个向量有一个缩放方法,那么如果它有一个返回值,我会假设它返回一个由输入缩放的新向量,如果没有,那么我会期望它在内部进行缩放。
  • 另外,如果类被扩展,当然你会遇到问题,在你链接的中途,你的对象被强制转换为超类型。当在父类中声明链接方法但在子类的实例上使用时,会发生这种情况。

好处

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

    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();
    

NBMyVector3d没有被用作真正的 Java 类,而是假定在.multiply()调用方法时执行叉积。.cross()不使用,以便那些不熟悉向量微积分的人更清楚“意图”
NB Amit 的解决方案是使用多行方法链接的第一个答案,为了完整起见,我将其作为第四个要点的一部分

于 2013-06-07T09:04:21.990 回答
24

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

第 1 步:Fluent Interface 与 Command-Query API

必须针对命令查询 API 考虑流畅的接口。为了更好地理解它,让我在下面编写命令查询 API 的项目符号列表定义。简而言之,这只是一种标准的面向对象的编码方法:

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

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

  • 查看面向对象的代码,您就会明白发生了什么。
  • 代码的调试更容易,因为每个调用都是单独发生的。

第 2 步:基于 Command-Query API 的流畅接口

但是命令查询 API 出于某种原因而存在,而且它确实读起来更好。那么我们如何同时拥有 fluent interface 和 command-query API 的优势呢?

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

通常,在命令查询 API 准备好(编写,可能经过单元测试,经过打磨以易于调试)之后,您可以在其之上编写一个流畅的接口软件层。换句话说,流利的接口通过使用命令查询API来完成它的功能。然后,在任何需要方便和可读性的地方使用流畅的接口(带有方法链接)。但是,一旦您想了解实际发生的情况(例如,在调试异常时),您总是可以深入研究命令查询 API——良好的面向对象的旧代码。

于 2013-06-13T12:53:58.127 回答
8

我发现使用方法链接的缺点是在发生NullPointerException或任何其他Exception情况时调试代码。假设您有以下代码:

String test = "TestMethodChain"; test.substring(0,10).charAt(11); //This is just an example

然后你会得到 String index out of range: 执行上述代码时的异常。当您进入实时情况并发生此类事情时,您会知道发生了哪一行错误,而不是链接方法的哪一部分导致了它。因此,在知道数据总会出现或错误得到正确处理的情况下,需要明智地使用它。

它也有它的优点,比如你不需要编写多行代码和使用多个变量。

许多框架/工具像 Dozer 一样使用它,当我过去调试代码时,我必须查看链的每个部分以找出导致错误的原因。

希望这可以帮助。

于 2013-06-07T05:57:20.827 回答
5

您可能想阅读Martin Fowler 的 Fluent Interface

概括地说

  • 不要链接方法,主要是因为它违反了命令查询责任分离 (CQRS) 设计原则。
  • 通过使其更接近业务讨论这些操作的方式来改进 api 设计,将它们视为内部 DSL
  • 尝试避免链接独立的方法,因为它们会污染 API,并且可能无法向代码的客户端/维护者揭示意图
于 2013-06-11T12:26:31.250 回答
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));

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

于 2013-06-07T08:05:02.617 回答
2

如果您更喜欢不变性函数式编程,那么您永远不会回来void

没有返回值的函数仅因其副作用而被调用。

当然,在某些情况下这是不适用的,但是void可以将返回的函数视为以不同方式尝试它的提示。

于 2013-06-07T08:37:33.623 回答
1

个人认为这是一个非常有用的模式,但不应该到处使用。考虑你有copyTo(T other)方法的情况。通常你会期望它不返回任何东西,但是如果它返回一个相同类型的对象,你会期望哪个对象?此类问题可以通过文档解决,但在方法签名上仍然模棱两可。

public class MyObject {

    // ... my data

    public MyObject copyTo(MyObject other) {

        //... copy data


        // what should I return?
        return this;
    }
}
于 2013-06-07T04:26:11.453 回答
1

这是相当大的工作量。特别是在涉及继承时。看看这篇关于构建器模式的精彩文章

当您期望客户端将在同一个实例上按顺序调用多个方法时,实现它很有意义,例如构建器模式、不可变对象等。但在我看来,在大多数情况下,这并不值得付出额外的努力。

于 2013-06-07T05:12:51.650 回答
0

对象具有属性和方法。每种方法都实现了对象整体目的的一部分。某些类型的方法,如构造函数、getter 和 setter,正在执行属性和对象本身的生命周期管理。其他方法返回对象的状态及其属性。这些方法通常是非无效的。

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

ad 1. 它们属于对象的内部工作。ad 2. 参数的信息用于在方法内执行一些工作。方法完成后,对象本身和参数之一的内部状态都没有变化。

但是为什么你应该让一个方法返回 void 并且为什么不让一个布尔值(成功与否)?方法返回 void(如在 setter 中)的动机是该方法旨在没有副作用。返回 true 或 false 可能是一个副作用。void 表示该方法在按设计执行时没有副作用。返回异常的 void 方法是好的,因为异常不是方法正常处理的副作用。

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

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

在我看来,要链接的无效方法仅是那些具有风味 2 并共享某种处理类型的方法。我不会链接有关类生命周期的 void 方法,因为每个方法都是对象整个状态的不同部分的更改。这些方法的存在会随着类设计的任何变化而随时间而变化,因此我不建议链接这些方法。此外,第一种类型的 void 方法引发的所有异常都是相互独立的,而您可能会期望共享某种处理类型的第 2 类链式 void 方法引发的异常有一些共同点。

于 2013-06-13T13:03:49.397 回答