方法链 – 为什么这是一个很好的做法,或不是?

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

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

这似乎被认为是一个很好的做法,因为它产生可读的代码或“stream畅的界面”。 然而,对我来说,它反而似乎打破了面向对象本身所暗示的对象调用符号 – 结果代码并不表示对前一个方法的结果执行操作,这是面向对象代码通常如何工作的结果:

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

这种差异为“调用结果对象”的点符号创build了两个不同的含义:在链接的上下文中,上面的例子会读为保存参与者对象,即使这个例子实际上是为了保存调度由getSchedule接收的对象。

我明白这里的区别在于被调用的方法是否应该返回某些东西(在这种情况下,它会返回被调用的对象本身进行链接)。 但是,这两种情况与标注本身并没有区别,只能从被调用方法的语义上区分。 当没有使用方法链时,我总是可以知道一个方法调用的操作与前一个调用的结果有关 – 链接,这个假设中断,我必须在语义上处理整个链,以了解实际对象叫真的是。 例如:

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

最后两个方法调用引用getSocialStream的结果,而之前的方法引用参与者。 实际上,在上下文发生变化的情况下编写链接可能是不好的做法,但即使这样,您也必须经常检查看起来相似的点链是否实际上保持在同一个上下文中,或者只处理结果。

对我来说,虽然方法链接表面上确实产生可读的代码,但是重载点符号的含义只会导致更多的混淆。 因为我不认为自己是编程专家,所以我认为这是我的错。 所以:我错过了什么? 我理解方法链接有点不对吗? 有没有一些方法链接特别好的地方,或者有些地方特别糟糕?

旁注:我知道这个问题可以被理解为一个被掩盖为一个问题的意见陈述。 但是,它并不是 – 我真的很想理解为什么链接被认为是好的做法,我认为它打破了固有的面向对象的表示法,在哪里出错呢?

我同意这是主观的。 大多数情况下,我避免了方法链接,但是最近我也发现了一个正确的事情 – 我接受了一个类似于10个参数的东西,并且需要更多的东西,但是大多数情况下,你只需要指定一个less数。 重写这个变得非常麻烦,非常快。 相反,我select了链式方法:

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

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

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

只是我的2美分;

方法链接使得debugging技巧变得棘手: – 你不能把断点放在一个简洁的点上,这样你就可以将程序正确地暂停在你想要的地方 – 如果其中一个方法抛出一个exception,并且你得到了一个行号,哪个方法在“连锁”造成的问题。

我认为总是写出非常简短的线条是很好的做法。 每一行应该只是一个方法调用。 喜欢更多的线条更长的线条。

编辑:评论提到,方法链和断线是分开的。 那是真实的。 不过,根据debugging器的不同,在语句中间可能会也可能不会有中断点。 即使可以,使用具有中间variables的单独行也会提供更多的灵活性,并且可以在Watch窗口中查看大量的值,这有助于debugging过程。

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

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

当我的例子中,一个或多个链接方法将返回除foo以外的任何对象时,我不使用它。 虽然语法上你可以链接任何东西,只要你在链中使用正确的API对象,改变对象恕我直言,使事情变得更不可读,如果不同的对象的API有任何相似之处,可以真正混淆。 如果你最后做一些真正常见的方法调用( .toString() .print() ,不pipe)你最终采取什么对象? 有人随便阅读代码可能无法捕捉到,它将是一个链中的隐式返回对象,而不是原始引用。

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

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

…作为一名外部开发人员,我们无法保证getSchedule实际上会返回一个有效的非空调度对象。 另外,debugging这种types的代码往往要困难得多,因为许多IDE在debugging时不会将方法调用评估为可检查的对象。 国际海事组织,任何时候你可能需要一个对象来检查debugging的目的,我喜欢有一个明确的variables。

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

方法链接

何时使用它

方法链接可以增加内部DSL的可读性,因此在某些思想中几乎成为内部DSL的同义词。 但是,链接方式与其他function组合结合使用时效果最好。

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

方法链接最大的问题是完成问题。 虽然有解决方法,但通常如果遇到这种情况,最好使用嵌套函数。 嵌套函数也是一个更好的select,如果你陷入与上下文variables混乱。

在我看来,方法链是一个新鲜的东西。 当然,它看起来很酷,但我没有看到任何真正的优势。

怎么:

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

任何比:

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

当addObject()返回一个新的对象时,exception可能是这样,在这种情况下,非链式代码可能会更麻烦一些:

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

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

我会举一个例子:

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

所以当你调用foodStore.getLocalStore(parameters).getPriceforProduct(任何东西)

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

如果getPriceforProduct(任何东西)不断变化,则不仅需要更改FoodStore,还需要更改名为链接方法的类。

你应该总是把目标放在class级之间。

这就是说,我个人喜欢在编程Ruby时链接它们。

链接的好处
即,我喜欢使用它的地方

链接的一个好处,我没有看到提到的是在variables启动时使用它的能力,或者当传递一个新的对象到一个方法,不知道这是否是不好的做法。

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

 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 

并说,你没有访问基类,或者说默认值是dynamic的,基于时间等。是的,你可以实例化,然后改变值,但可能会变得繁琐,特别是如果你只是通过一个方法的值:

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

但是这不是更容易阅读:

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

或者,一个class级成员呢?

 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 

这就是我一直使用链接的方式,通常我的方法只是用于configuration,所以它们只有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) 

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

当有很多parameter passing给例程的时候,我不使用链接,主要是因为行很长,而且OP提到它会在你调用其他类的例程传递给链接方法。

还有一个问题,例程会返回无效的数据,所以到目前为止,我只用了链接,当我返回被调用的同一个实例。 正如你指出的,如果你在类之间进行链接,你会使得debugging困难(哪一个返回null),并且可以增加类之间的依赖关系。

结论

就像生活中的一切,编程一样,Chaining既不好也不坏,如果能避免不好的话链接可以是一大好处。

我试图遵循这些规则。

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

方法链可以允许直接在Java中devise高级DSL 。 实质上,您可以build模至less以下这些types的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中实现诸如SQL之类的复杂的DSL,就像我创build的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 ); 

这似乎有点主观。

方法链不是天生就是坏的或好的。

可读性是最重要的。

(另外考虑一下,如果有东西发生变化,链接的方法就会变得非常脆弱)

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

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

在大多数情况下,方法链可能只是一个新颖的东西,但我认为它是有用的。 在CodeIgniter的Active Record中可以find一个例子:

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

这看起来更清洁(在我看来,更有意义)比:

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

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

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

之前:

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

后:

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

在“之前”的实现function修改了对象,并return this作为return this 。 我改变了实现返回一个相同types的新对象

我对这个变化的推理

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

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

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

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

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

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

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

  6. 使用简短的variables名称也可以减less代码混乱,而不会在模型上强制使用api。

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

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

这里完全错过的一点是,方法链允许 。 这是“with”(在某些语言中执行不力)的有效替身。

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

DRY总是很重要的,这也是重要的。 如果A原来是一个错误,而这些操作需要在B上执行,则只需要在1个地方更新,而不是3个。

实际上,在这种情况下,优势很小。 尽pipe如此,打字less一点,一个更健壮(DRY),我会拿它。

好:

  1. 它很简洁,但是可以让你把更多的东西放在一条线上。
  2. 有时你可以避免使用一个variables,这个variables偶尔会有用。
  3. 它可能performance更好。

不好:

  1. 你正在实现返回,实质上是在方法上添加了一些function,而这些function并不属于这些方法的一部分。 它返回的东西,你已经纯粹保存几个字节。
  2. 当一条链路通向另一条链路时,它隐藏了上下文切换。 你可以用getter来获得这个,除非上下文切换的时候非常清楚。
  3. 链接在多行上看起来很难看,不好用于缩进,并可能导致某些操作员处理混淆(特别是在ASI语言中)。
  4. 如果你想开始返回一些对链式方法有用的东西,那么你将很难修复它,或者碰到更多的问题。
  5. 您正在将控制权卸载到您通常不会为了便利而卸载的实体,即使在严格types的语言中,也不能总是检测到由此导致的错误。
  6. 它可能performance更差。

一般:

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

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

在accasation它可以被误用,而不是另一种方法(例如传递一个数组)或以奇怪的方式(parent.setSomething()。getChild()。setSomething()。getParent()。setSomething())混合方法。