方法链 - 为什么这是一个好的做法?

时间:2009-07-09 13:44:58

标签: oop fluent-interface method-chaining

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

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

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

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

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

我理解这里的区别在于是否应该调用被调用的方法返回某些内容(在这种情况下,它将返回被调用对象本身以进行链接)。但是这两种情况与符号本身无法区分,只能从被调用方法的语义中区分出来。当不使用方法链接时,我总是可以知道方法调用对与前一个调用的 result 相关的东西进行操作 - 使用链接,这个假设会中断,我必须在语义上处理整个链了解实际被调用的对象是什么。例如:

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

最后两个方法调用引用getSocialStream的结果,而之前引用的是参与者。也许在上下文发生变化的情况下实际编写链是不好的做法(是吗?),但即使这样,你也要不断检查看起来相似的点链是否实际上保持在同一个上下文中,或者只对结果起作用

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

旁注:我理解这个问题可以理解为一个被掩盖为问题的意见陈述。然而,它并非如此 - 我真的想要理解为什么链接被认为是良好的实践,以及在认为它打破固有的面向对象的符号时我会在哪里出错。

18 个答案:

答案 0 :(得分:75)

只需2美分;

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

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

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

答案 1 :(得分:70)

我同意这是主观的。在大多数情况下,我避免使用方法链接,但最近我还发现了一个正确的情况 - 我有一个接受类似10个参数的方法,并且需要更多,但是在大多数时候你只需要指定一个少数。随着覆盖,这变得非常麻烦,非常快。相反,我选择了链式方法:

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

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

重点是 - 在99%的情况下,如果没有方法链接,你可以做得更好甚至更好。但是这是最佳方法的1%。

答案 2 :(得分:38)

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

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

在我的示例中,当一个或多个链接方法返回除foo之外的任何对象时,我不使用它。虽然在语法上你可以链接任何东西,只要你在链中使用正确的API,更改对象IMHO会使事情变得不那么容易,如果不同对象的API有任何相似之处,可能会让人感到困惑。如果你最后做了一些非常常见的方法调用(.toString().print(),无论如何)你最终会采取哪些对象?随便读取代码的人可能无法发现它将是链中隐式返回的对象而不是原始引用。

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

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

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

答案 3 :(得分:23)

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

  

Method Chaining

     

何时使用

     

方法链接可以添加很多东西   内部DSL的可读性   结果几乎成了一个   一些内部DSL的synonum   头脑。方法链接最好,   但是,当它结合使用时   与其他功能组合。

     

方法链接尤其如此   对于像parent :: =这样的语法有效   (这|那)*。使用方法不同   方法提供了可读的方法   看看接下来会发生哪种争论。   类似的可选参数也可以   使用Method轻松跳过   链接。强制性条款清单,   例如parent :: = first second不会   与基本形式一起工作得很好,   虽然它可以得到很好的支持   使用渐进式界面。大多数   我喜欢嵌套功能的时间   对于那种情况。

     

Method最大的问题   链接是完成问题。   虽然通常有变通方法   如果你碰到这个你会更好   使用嵌套函数。嵌套   功能也是更好的选择   你陷入了困境   上下文变量。

答案 4 :(得分:20)

在我看来,方法链接有点新奇。当然,它看起来很酷,但我认为没有任何真正的优势。

如何:

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")

答案 5 :(得分:8)

这很危险,因为你可能依赖于比预期更多的对象,就像你的调用返回另一个类的实例一样:

我举一个例子:

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

所以当你调用foodStore.getLocalStore(参数).getPriceforProduct(nothing)

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

如果getPriceforProduct(任何东西)发生了变化,你不仅需要更改FoodStore,还需要更改调用链式方法的类。

你应该始终瞄准课间的松耦合。

话虽这么说,我个人喜欢在编写Ruby时链接它们。

答案 6 :(得分: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

VS

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

相似
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. 使代码更简单时使用

答案 7 :(得分:6)

方法链接可以直接在Java中设计高级DSLs。实质上,您至少可以建模这些类型的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所做的那样。请参阅此处从my blog获取的相当复杂的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,一个用于直接用Java来终止RTF文档的DSL。一个例子:

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

答案 8 :(得分:6)

这似乎有点主观。

方法链接不是那种天生就是坏或好的imo。

可读性是最重要的事情。

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

答案 9 :(得分:6)

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

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

答案 10 :(得分:3)

对于大多数情况来说,方法链接可能只是一个新奇事物,但我认为它有它的位置。可以在CodeIgniter's Active Record use中找到一个示例:

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

在我看来,这看起来更清晰(而且更有意义),而不是:

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

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

答案 11 :(得分:3)

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

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

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

还有:

<强> convertTextToVoice.LoadText( “的Source.txt”)ConvertToVoice( “destination.wav”);

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

public void foo(params object [] items)

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

答案 12 :(得分:2)

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

在:

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

后:

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

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

我对此更改的理由

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

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

    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()
    
  7. <强>结论:
    在我看来,使用return this实现的流畅界面是错误的。

答案 13 :(得分:1)

这里完全错过了点,方法链接允许 DRY 。它是“with”的有效替身(在某些语言中实现得很差)。

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

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

这与DRY始终重要的原因相同;如果A证明是错误,并且需要在B上执行这些操作,则只需要在1个位置更新,而不是3个。

实际上,在这种情况下,优势很小。不过,打字少一点,痘痘更健壮(DRY),我会接受它。

答案 14 :(得分:1)

我通常讨厌方法链接,因为我认为它会降低可读性。紧凑性经常与可读性相混淆,但是它们不是相同的术语。如果您在单个语句中执行所有操作,则这很紧凑,但与在多个语句中执行操作相比,大多数时候它的可读性(难于跟踪)低。如您所见,除非不能保证所用方法的返回值相同,否则方法链接将引起混乱。

1。)

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

vs

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

2。)

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

vs

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

3。)

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

vs

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,因为在这种情况下,松散耦合更为重要。

答案 15 :(得分:0)

好处:

  1. 它很简洁,但却可以让你更优雅地融入一条线。
  2. 您有时可以避免使用变量,这可能偶尔会有用。
  3. 可能表现更好。
  4. 坏事:

    1. 您正在实现返回,实质上是为对象上的方法添加功能,而这些功能实际上并不是这些方法的一部分。它正在返回你已经拥有的东西,纯粹是为了节省几个字节。
    2. 当一条链通向另一条链时,它隐藏了上下文切换。你可以用getter来解决这个问题,除非上下文切换时非常清楚。
    3. 多行链接看起来很难看,不能很好地处理缩进,并且会导致某些操作员处理混乱(特别是在使用ASI的语言中)。
    4. 如果你想开始返回对链式方法有用的其他东西,你可能会更难修复它或遇到更多问题。
    5. 您正在将控制权转移到一个您通常不会卸载的实体,这纯粹是为了方便,即使在严格打印的语言中也不会总是检测到错误。
    6. 可能表现更差。
    7. 一般:

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

      链接会在某些情况下严重影响可读性,特别是在第1点和第2点称重时。

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

答案 16 :(得分:0)

有根据的答案

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

一些问题:

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

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

根据语言和编译器的不同,编译时间可能会更慢,因为表达式的解析要复杂得多。

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

答案 17 :(得分:0)

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

import Participant
import Schedule

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

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

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