171

方法链接是对象方法返回对象本身以便为另一个方法调用结果的做法。像这样:

participant.addSchedule(events[1]).addSchedule(events[2]).setStatus('attending').save()

这似乎被认为是一种很好的做法,因为它产生了可读的代码或“流畅的界面”。然而,对我来说,它似乎打破了面向对象本身隐含的对象调用符号 - 生成的代码并不代表对先前方法的结果执行操作,这就是面向对象代码通常预期的工作方式:

participant.getSchedule('monday').saveTo('monnday.file')

这种差异设法为“调用结果对象”的点表示法创建了两种不同的含义:在链接的上下文中,上面的示例将读取为保存参与者对象,即使该示例实际上是为了保存计划getSchedule 接收到的对象。

我知道这里的区别在于被调用的方法是否应该返回某些东西(在这种情况下,它会返回被调用的对象本身以进行链接)。但这两种情况与符号本身没有区别,仅与被调用方法的语义不同。当不使用方法链接时,我总是可以知道方法调用对与前一次调用的结果相关的东西进行操作- 使用链接,这个假设会被打破,我必须在语义上处理整个链以了解实际对象是什么所谓真的是。例如:

participant.attend(event).setNotifications('silent').getSocialStream('twitter').postStatus('Joining '+event.name).follow(event.getSocialId('twitter'))

最后两个方法调用引用 getSocialStream 的结果,而前面的调用引用参与者。也许在上下文发生变化的地方实际编写链是不好的做法(是吗?),但即使那样,您也必须不断检查看起来相似的点链是否实际上保持在相同的上下文中,或者只处理结果.

在我看来,虽然方法链接在表面上确实产生了可读的代码,但重载点符号的含义只会导致更多的混乱。因为我不认为自己是编程大师,所以我认为是我的错。所以:我错过了什么?我是否理解方法链接有些错误?在某些情况下方法链接特别好,或者有些情况特别糟糕?

旁注:我理解这个问题可以被解读为一种被掩盖为问题的观点陈述。然而,事实并非如此——我真的很想理解为什么链接被认为是好的实践,以及我认为它破坏了固有的面向对象表示法的错误在哪里。

4

18 回答 18

88

我同意这是主观的。在大多数情况下,我避免了方法链接,但最近我也发现了一个正确的案例——我有一个方法可以接受 10 个参数,并且需要更多参数,但大多数时候你只需要指定一个很少。有了覆盖,这很快就变得非常麻烦。相反,我选择了链接方法:

MyObject.Start()
    .SpecifySomeParameter(asdasd)
    .SpecifySomeOtherParameter(asdasd)
    .Execute();

方法链接方法是可选的,但它使编写代码更容易(尤其是使用 IntelliSense)。请注意,这是一个孤立的案例,并不是我的代码中的一般做法。

关键是 - 在 99% 的情况下,如果没有方法链接,您可能会做得同样好,甚至更好。但是有 1% 是最好的方法。

于 2009-07-09T14:00:05.847 回答
84

只是我的 2 美分;

方法链使调试变得棘手: - 你不能将断点放在一个简洁的点,这样你就可以在你想要的地方暂停程序 - 如果这些方法之一抛出异常,并且你得到一个行号,你不知道“链”中的哪个方法导致了问题。

我认为总是写非常简短的行通常是一个好习惯。每一行都应该只进行一次方法调用。喜欢更多的行而不是更长的行。

编辑:评论提到方法链接和换行是分开的。那是真实的。但是,根据调试器的不同,可能会也可能不会在语句中间放置断点。即使可以,使用带有中间变量的单独行也为您提供了更多的灵活性,并且您可以在 Watch 窗口中检查有助于调试过程的一大堆值。

于 2010-10-28T03:56:26.200 回答
46

就个人而言,我更喜欢仅作用于原始对象的链接方法,例如设置多个属性或调用实用程序类型的方法。

foo.setHeight(100).setWidth(50).setColor('#ffffff');
foo.moveTo(100,100).highlight();

在我的示例中,当一个或多个链接方法将返回除 foo 之外的任何对象时,我不使用它。虽然从语法上讲,只要您为链中的对象使用正确的 API,您就可以链接任何东西,但恕我直言,更改对象会使事情变得不那么可读,并且如果不同对象的 API 有任何相似之处,可能会非常混乱。如果你在最后做了一些非常常见的方法调用(.toString(), .print(),无论如何)你最终会作用于哪个对象?随便阅读代码的人可能不会发现它是链中隐式返回的对象,而不是原始引用。

链接不同的对象也可能导致意外的空错误。在我的示例中,假设foo是有效的,那么所有的方法调用都是“安全的”(例如,对 foo 有效)。在 OP 的示例中:

participant.getSchedule('monday').saveTo('monnday.file')

...无法保证(作为查看代码的外部开发人员) getSchedule 实际上会返回一个有效的、非空的计划对象。此外,调试这种代码风格通常要困难得多,因为许多 IDE 不会在调试时将方法调用评估为您可以检查的对象。IMO,任何时候您可能需要一个对象来检查以进行调试,我更喜欢将它放在一个显式变量中。

于 2009-07-10T14:42:33.137 回答
27

Martin Fowler 在这里有一个很好的讨论:

方法链

何时使用

方法链可以极大地增加内部 DSL 的可读性,因此在某些人看来几乎已成为内部 DSL 的代名词。然而,当与其他函数组合结合使用时,方法链是最好的。

方法链对于像 parent::= (this | that)* 这样的语法特别有效。不同方法的使用提供了查看下一个参数的可读方式。类似地,可选参数可以很容易地通过方法链跳过。强制性子句列表,例如 parent::= first second 不能很好地与基本形式一起使用,尽管使用渐进式接口可以很好地支持它。大多数情况下,我更喜欢嵌套函数。

Method Chaining 最大的问题是完成问题。虽然有一些变通方法,但通常如果遇到这种情况,最好使用嵌套函数。如果您对上下文变量感到困惑,嵌套函数也是一个更好的选择。

于 2009-07-09T14:09:03.637 回答
24

在我看来,方法链有点新奇。当然,它看起来很酷,但我看不出它有什么真正的优势。

怎么:

someList.addObject("str1").addObject("str2").addObject("str3")

比:

someList.addObject("str1")
someList.addObject("str2")
someList.addObject("str3")

例外情况可能是 addObject() 返回一个新对象时,在这种情况下,未链接的代码可能会更麻烦一些,例如:

someList = someList.addObject("str1")
someList = someList.addObject("str2")
someList = someList.addObject("str3")

编辑:在过去的 10 年里,我对此的看法发生了变化。对于可变对象,我仍然看不到很多好处,尽管它对于避免一点点重复很有用。但是现在我更喜欢不变性,方法链是我进行非破坏性更新的首选方式,我一直在使用它。

于 2009-07-09T13:52:43.530 回答
8

许多人使用方法链接作为一种方便的形式,而不是考虑任何可读性问题。如果方法链接涉及对同一个对象执行相同的操作,则它是可以接受的——但前提是它实际上增强了可读性,而不仅仅是为了编写更少的代码。

不幸的是,根据问题中给出的示例,许多使用方法链接。虽然它们仍然可以读取,但不幸的是它们会导致多个类之间的高度耦合,因此这是不可取的。

于 2009-07-10T03:31:25.850 回答
7

这是危险的,因为您可能依赖于比预期更多的对象,例如您的调用返回另一个类的实例:

我举个例子:

foodStore 是一个由您拥有的许多食品商店组成的对象。foodstore.getLocalStore() 返回一个对象,该对象包含与参数最近的商店的信息。getPriceforProduct(anything) 是该对象的一种方法。

所以当你打电话给 foodStore.getLocalStore(parameters).getPriceforProduct(anything)

您不仅依赖于 FoodStore,还依赖于 LocalStore。

如果 getPriceforProduct(anything) 发生变化,您不仅需要更改 FoodStore,还需要更改调用链式方法的类。

您应该始终以类之间的松散耦合为目标。

话虽如此,我个人喜欢在编写 Ruby 时将它们链接起来。

于 2009-07-09T16:07:49.177 回答
6

这似乎有点主观。

方法链接并不是本质上是坏或好的东西。

可读性是最重要的。

(还要考虑,如果发生变化,链接大量方法会使事情变得非常脆弱)

于 2009-07-09T13:51:22.940 回答
6

链接的好处,
即我喜欢在哪里使用它

我没有提到的链接的一个好处是能够在变量启动期间使用它,或者在将新对象传递给方法时,不确定这是否是不好的做法。

我知道这是人为的例子,但说你有以下课程

Public Class Location
   Private _x As Integer = 15
   Private _y As Integer = 421513

   Public Function X() As Integer
      Return _x
   End Function
   Public Function X(ByVal value As Integer) As Location
      _x = value
      Return Me
   End Function

   Public Function Y() As Integer
      Return _y
   End Function
   Public Function Y(ByVal value As Integer) As Location
      _y = value
      Return Me
   End Function

   Public Overrides Function toString() As String
      Return String.Format("{0},{1}", _x, _y)
   End Function
End Class

Public Class HomeLocation
   Inherits Location

   Public Overrides Function toString() As String
      Return String.Format("Home Is at: {0},{1}", X(), Y())
   End Function
End Class

并说您无权访问基类,或者说默认值是动态的,基于时间等。是的,您可以实例化然后更改值,但这可能会变得很麻烦,特别是如果您只是通过方法的值:

  Dim loc As New HomeLocation()
  loc.X(1337)
  PrintLocation(loc)

但这不是更容易阅读吗:

  PrintLocation(New HomeLocation().X(1337))

或者,班员呢?

Public Class Dummy
   Private _locA As New Location()
   Public Sub New()
      _locA.X(1337)
   End Sub
End Class

对比

Public Class Dummy
   Private _locC As Location = New Location().X(1337)
End Class

这就是我一直在使用链接的方式,通常我的方法只是用于配置,所以它们只有 2 行长,设置一个值,然后Return Me. 对我们来说,它已经将难以阅读和理解的大行代码清理成一行,读起来就像一个句子。就像是

New Dealer.CarPicker().Subaru.WRX.SixSpeed.TurboCharged.BlueExterior.GrayInterior.Leather.HeatedSeats

VS 类似的东西

New Dealer.CarPicker(Dealer.CarPicker.Makes.Subaru
                   , Dealer.CarPicker.Models.WRX
                   , Dealer.CarPicker.Transmissions.SixSpeed
                   , Dealer.CarPicker.Engine.Options.TurboCharged
                   , Dealer.CarPicker.Exterior.Color.Blue
                   , Dealer.CarPicker.Interior.Color.Gray
                   , Dealer.CarPicker.Interior.Options.Leather
                   , Dealer.CarPicker.Interior.Seats.Heated)

链接的损害
,即我不喜欢使用它的地方

当有很多参数要传递给例程时,我不使用链接,主要是因为行变得很长,并且正如 OP 所提到的,当您将例程调用到其他类以传递给其中一个时,它会变得混乱链接方法。

还有人担心例程会返回无效数据,到目前为止,我只在返回被调用的同一个实例时才使用链接。正如所指出的,如果你在类之间进行链接,你会使调试变得更加困难(哪个返回 null?)并且可以增加类之间的依赖耦合。

结论

就像生活和编程中的一切一样,链接既不好也不坏,如果你能避免坏事,那么链接可能会带来很大的好处。

我尝试遵守这些规则。

  1. 尽量不要在类之间链接
  2. 制作专门用于链接的例程
  3. 在链接例程中只做一件事
  4. 当它提高可读性时使用它
  5. 当它使代码更简单时使用它
于 2011-05-20T18:59:21.363 回答
6

方法链可以允许直接在 Java 中设计高级DSL。本质上,您至少可以对这些类型的 DSL 规则进行建模:

1. SINGLE-WORD
2. PARAMETERISED-WORD parameter
3. WORD1 [ OPTIONAL-WORD]
4. WORD2 { WORD-CHOICE-A | WORD-CHOICE-B }
5. WORD3 [ , WORD3 ... ]

这些规则可以使用这些接口来实现

// Initial interface, entry point of the DSL
interface Start {
  End singleWord();
  End parameterisedWord(String parameter);
  Intermediate1 word1();
  Intermediate2 word2();
  Intermediate3 word3();
}

// Terminating interface, might also contain methods like execute();
interface End {}

// Intermediate DSL "step" extending the interface that is returned
// by optionalWord(), to make that method "optional"
interface Intermediate1 extends End {
  End optionalWord();
}

// Intermediate DSL "step" providing several choices (similar to Start)
interface Intermediate2 {
  End wordChoiceA();
  End wordChoiceB();
}

// Intermediate interface returning itself on word3(), in order to allow for
// repetitions. Repetitions can be ended any time because this interface
// extends End
interface Intermediate3 extends End {
  Intermediate3 word3();
}

使用这些简单的规则,您可以直接在 Java 中实现复杂的 DSL,例如 SQL,正如我创建的库jOOQ所做的那样。请参阅我的博客中的一个相当复杂的 SQL 示例:

create().select(
    r1.ROUTINE_NAME,
    r1.SPECIFIC_NAME,
    decode()
        .when(exists(create()
            .selectOne()
            .from(PARAMETERS)
            .where(PARAMETERS.SPECIFIC_SCHEMA.equal(r1.SPECIFIC_SCHEMA))
            .and(PARAMETERS.SPECIFIC_NAME.equal(r1.SPECIFIC_NAME))
            .and(upper(PARAMETERS.PARAMETER_MODE).notEqual("IN"))),
                val("void"))
        .otherwise(r1.DATA_TYPE).as("data_type"),
    r1.NUMERIC_PRECISION,
    r1.NUMERIC_SCALE,
    r1.TYPE_UDT_NAME,
    decode().when(
    exists(
        create().selectOne()
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.notEqual(r1.SPECIFIC_NAME))),
        create().select(count())
            .from(r2)
            .where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
            .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
            .and(r2.SPECIFIC_NAME.lessOrEqual(r1.SPECIFIC_NAME)).asField())
    .as("overload"))
.from(r1)
.where(r1.ROUTINE_SCHEMA.equal(getSchemaName()))
.orderBy(r1.ROUTINE_NAME.asc())
.fetch()

另一个很好的例子是jRTF,一个小 DSL 设计用于直接在 Java 中创建 RTF 文档。一个例子:

rtf()
  .header(
    color( 0xff, 0, 0 ).at( 0 ),
    color( 0, 0xff, 0 ).at( 1 ),
    color( 0, 0, 0xff ).at( 2 ),
    font( "Calibri" ).at( 0 ) )
  .section(
        p( font( 1, "Second paragraph" ) ),
        p( color( 1, "green" ) )
  )
).out( out );
于 2012-01-05T15:40:37.207 回答
4

在大多数情况下,方法链接可能只是一个新事物,但我认为它有它的位置。在CodeIgniter 的 Active Record 使用中可以找到一个示例:

$this->db->select('something')->from('table')->where('id', $id);

这看起来比:

$this->db->select('something');
$this->db->from('table');
$this->db->where('id', $id);

这确实是主观的;每个人都有自己的看法。

于 2009-07-10T12:58:55.407 回答
3

我认为主要的谬误是认为这是一种面向对象的方法,而实际上它更像是一种函数式编程方法而不是其他任何方法。

我使用它的主要原因是为了可读性和防止我的代码被变量淹没。

当他们说它损害可读性时,我真的不明白其他人在说什么。它是我用过的最简洁、最有凝聚力的编程形式之一。

还有这个:

convertTextToVoice.LoadText("source.txt").ConvertToVoice("destination.wav");

是我通常会如何使用它。使用它来链接 x 个参数不是我通常使用它的方式。如果我想在方法调用中放入 x 个参数,我会使用params语法:

公共无效 foo(参数对象 [] 项目)

并根据您的用例根据类型转换对象或仅使用数据类型数组或集合。

于 2018-01-05T18:31:47.620 回答
3

我通常讨厌方法链接,因为我认为它会降低可读性。紧凑性经常与可读性混淆,但它们不是同一个术语。如果你在一个语句中做所有事情,那么它是紧凑的,但它在大多数时候比在多个语句中做它更不可读(更难理解)。正如您所注意到的,除非您不能保证使用的方法的返回值相同,否则方法链接将成为混乱的根源。

1.)

participant
    .addSchedule(events[1])
    .addSchedule(events[2])
    .setStatus('attending')
    .save();

对比

participant.addSchedule(events[1]);
participant.addSchedule(events[2]);
participant.setStatus('attending');
participant.save()

2.)

participant
    .getSchedule('monday')
        .saveTo('monnday.file');

对比

mondaySchedule = participant.getSchedule('monday');
mondaySchedule.saveTo('monday.file');

3.)

participant
    .attend(event)
    .setNotifications('silent')
    .getSocialStream('twitter')
        .postStatus('Joining '+event.name)
        .follow(event.getSocialId('twitter'));

对比

participant.attend(event);
participant.setNotifications('silent')
twitter = participant.getSocialStream('twitter')
twitter.postStatus('Joining '+event.name)
twitter.follow(event.getSocialId('twitter'));

如您所见,您几乎没有获胜,因为您必须在单个语句中添加换行符以使其更具可读性,并且您必须添加缩进以清楚地表明您正在谈论不同的对象。好吧,如果我想使用基于标识的语言,那么我会学习 Python 而不是这样做,更不用说大多数 IDE 会通过自动格式化代码来删除缩进。

我认为这种链接唯一有用的地方是在 CLI 中管道流或在 SQL 中将多个查询连接在一起。两者都有多个陈述的价格。但是,如果你想解决复杂的问题,即使付出代价并使用变量在多个语句中编写代码,或者编写 bash 脚本和存储过程或视图,你最终也会失败。

作为 DRY 的解释:“避免知识的重复(而不是文本的重复)。” 和“少打字,不要重复文字。”,第一个原则的真正含义,但第二个是常见的误解,因为很多人无法理解过于复杂的废话,例如“每条知识必须有一个单一的,明确的,系统内的权威表示”。第二个是不惜一切代价的紧凑性,这在这种情况下会中断,因为它会降低可读性。当您在有界上下文之间复制代码时,第一种解释会被 DDD 打破,因为松耦合在这种情况下更为重要。

于 2019-02-16T22:00:15.127 回答
2

我同意,因此我改变了在我的库中实现流畅接口的方式。

前:

collection.orderBy("column").limit(10);

后:

collection = collection.orderBy("column").limit(10);

在“之前”实现中,函数修改了对象并以return this. 我更改了实现以返回相同类型的新对象

我对这种变化的理由

  1. 返回值与函数无关,纯粹是为了支持链接部分,根据OOP应该是一个void函数。

  2. 系统库中的方法链也以这种方式实现(如 linq 或字符串):

    myText = myText.trim().toUpperCase();
    
  3. 原始对象保持不变,允许 API 用户决定如何处理它。它允许:

    page1 = collection.limit(10);
    page2 = collection.offset(10).limit(10);
    
  4. 复制实现也可用于构建对象:

    painting = canvas.withBackground('white').withPenSize(10);
    

    函数setBackground(color)更改实例并且不返回任何内容的位置(就像它应该的那样)

  5. 函数的行为更可预测(参见第 1 点和第 2 点)。

  6. 使用简短的变量名还可以减少代码混乱,而无需在模型上强制使用 api。

    var p = participant; // create a reference
    p.addSchedule(events[1]);p.addSchedule(events[2]);p.setStatus('attending');p.save()
    

结论:
在我看来,使用return this实现的流畅接口是错误的。

于 2011-08-27T14:26:35.500 回答
1

这里完全错过的一点是方法链接允许DRY。它是“with”(在某些语言中实现不佳)的有效替代品。

A.method1().method2().method3(); // one A

A.method1();
A.method2();
A.method3(); // repeating A 3 times

出于同样的原因,DRY 总是很重要。如果 A 出现错误,并且需要在 B 上执行这些操作,则只需要在 1 处进行更新,而不是在 3 处。

实际上,在这种情况下优势很小。尽管如此,打字少一点,健壮一点(干),我会接受的。

于 2012-04-01T03:03:47.667 回答
1

有意见的答案

链接的最大缺点是读者很难理解每个方法如何影响原始对象,如果影响,以及每个方法返回什么类型。

一些问题:

  • 链中的方法是返回一个新对象,还是同一个对象发生了变异?
  • 链中的所有方法都返回相同的类型吗?
  • 如果不是,当链中的类型发生变化时如何指示?
  • 最后一个方法返回的值可以安全丢弃吗?

在大多数语言中,使用链接确实会更难调试。即使链中的每一步都在自己的行上(这违背了链接的目的),也很难检查每一步之后返回的值,特别是对于非变异方法。

编译时间可能会更慢,具体取决于语言和编译器,因为表达式的解析可能要复杂得多。

我相信与所有事情一样,链接是一个很好的解决方案,在某些情况下可以派上用场。应谨慎使用,了解其含义,并将链元素的数量限制在几个。

于 2019-05-30T22:08:57.500 回答
0

好的:

  1. 它很简洁,但允许您优雅地将更多内容放入一行中。
  2. 您有时可以避免使用变量,这有时可能很有用。
  3. 它可能会表现得更好。

坏处:

  1. 您正在实现返回,本质上是向对象上的方法添加功能,而这些功能并不是这些方法的真正用途。它返回一些你已经拥有的东西,纯粹是为了节省几个字节。
  2. 当一条链通向另一条链时,它会隐藏上下文切换。您可以使用 getter 来获得它,除非上下文切换时非常清楚。
  3. 多行链接看起来很难看,不能很好地与缩进配合使用,并且可能会导致一些运算符处理混乱(尤其是在具有 ASI 的语言中)。
  4. 如果您想开始返回对链式方法有用的其他内容,您可能会更难修复它或遇到更多问题。
  5. 您正在将控制权卸载到一个您通常不会纯粹为了方便而卸载的实体,即使在严格类型的语言中也不能总是检测到由此引起的错误。
  6. 它可能表现更差。

一般的:

一个好的方法是在出现情况或特定模块特别适合它之前一般不使用链接。

在某些情况下,链接会严重损害可读性,尤其是在权衡第 1 点和第 2 点时。

在增加时,它可能会被滥用,例如代替另一种方法(例如传递数组)或以奇怪的方式混合方法(parent.setSomething().getChild().setSomething().getParent().setSomething())。

于 2017-09-29T14:22:49.490 回答
0

在类型化语言(缺少auto或等效)中,这使实现者不必声明中间结果的类型。

import Participant
import Schedule

Participant participant = new Participant()
... snip...
Schedule s = participant.getSchedule(blah)
s.saveTo(filename)

对于较长的链,您可能要处理几种不同的中间类型,您需要声明它们中的每一个。

我相信这种方法确实是在 Java 中开发的,其中 a) 所有函数调用都是成员函数调用,并且 b) 需要显式类型。当然,这里有一个权衡,失去了一些明确性,但在某些情况下,有些人认为这是值得的。

于 2020-07-17T19:19:26.320 回答