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

问题描述 投票:140回答:17

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

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的结果,而之前引用的是参与者。也许在上下文发生变化的情况下实际编写链是不好的做法(是吗?),但即使这样,你也必须不断检查看起来相似的点链是否实际上保持在同一个上下文中,或者只对结果起作用。

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

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

oop fluent-interface method-chaining
17个回答
71
投票

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

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

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

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


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

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

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


3
投票

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

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

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

这个:

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

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

public void foo(params object [] items)

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


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

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


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),我会接受它。


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破坏,因为松散耦合在该场景中更为重要。


0
投票

好处:

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

坏事:

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

一般:

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

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

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


0
投票

意见的答案

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

一些问题:

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

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

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

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


76
投票

只需2美分;

方法链接使调试变得棘手: - 你不能把断点放在一个简洁的点上,这样你就可以把程序准确地暂停到你想要的地方 - 如果其中一个方法抛出异常,你得到一个行号,你就不知道了“链”中的哪种方法引起了问题。

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

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


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,任何时候您可能需要一个对象来检查以进行调试,我更喜欢将它放在一个显式变量中。


23
投票

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

Method Chaining

什么时候使用它

方法链接可以增加内部DSL的可读性,因此在某些人的思想中几乎成为内部DSL的同步。方法链接最好,当它与其他功能组合一起使用时。

方法链接对于像parent :: =(this | that)*这样的语法特别有效。使用不同的方法提供了可见的方式来查看下一个参数。类似地,可以使用Method Chaining轻松跳过可选参数。强制子句列表(例如parent :: = first second)对基本表单的效果不佳,尽管使用渐进式接口可以很好地支持它。大多数时候我更喜欢这种情况下的嵌套函数。

Method Chaining的最大问题是整理问题。虽然有解决方法,但通常如果遇到这种情况,最好使用嵌套函数。如果您遇到上下文变量的混乱,嵌套函数也是更好的选择。


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

8
投票

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

我举个例子:

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

所以当你调用食物Store.getLocalStorage(参数).getPricefor产品(任何东西)

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

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

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

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


6
投票

这似乎有点主观。

方法链不是本质上坏或好的imo。

可读性是最重要的。

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


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?)并且可以增加类之间的依赖关系。

结论

就像生活和编程中的一切一样,Chaining既不好也不坏,如果你可以避免坏,那么链接可以是一个很大的好处。

我试着遵循这些规则。

  1. 尽量不要在类之间进行链接
  2. 制作专门用于链接的例程
  3. 链接例程中只做一件事
  4. 在提高可读性时使用它
  5. 在使代码更简单时使用它
© www.soinside.com 2019 - 2024. All rights reserved.