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

Ila*_*ste 147 oop fluent-interface method-chaining

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

participant.addSchedule(events[1]).addSchedule(events[2]).setStatus('attending').save()
Run Code Online (Sandbox Code Playgroud)

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

participant.getSchedule('monday').saveTo('monnday.file')
Run Code Online (Sandbox Code Playgroud)

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

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

participant.attend(event).setNotifications('silent').getSocialStream('twitter').postStatus('Joining '+event.name).follow(event.getSocialId('twitter'))
Run Code Online (Sandbox Code Playgroud)

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

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

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

RAY*_*RAY 76

只需2美分;

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

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

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

  • 是不是使用换行符和方法链接独立?您可以按照@ Vilx- answer在换行符上链接每个调用,并且通常可以在同一行上放置多个单独的语句(例如,在Java中使用分号). (3认同)
  • +1,在任何时候都不知道每个方法在步骤调试方法链时返回的内容.方法链模式是一种黑客攻击.这是维护程序员最糟糕的噩梦. (3认同)
  • 我也不热衷于链接,但为什么不简单地将断点放在方法定义中呢? (3认同)
  • 这是唯一正确的答案,应该是被接受的答案。该线程上的大多数其他答案仅与编写代码的过程有关。使用软件时,您必须考虑整个产品生命周期。公司在产品的整个生命周期中将花费更多的时间来维护代码,而不是实际编写代码。 (3认同)
  • 这是回复是完全合理的,但它只是显示了我所知道的所有调试器中存在的弱点,并且与该问题没有特别的关系. (2认同)

Vil*_*lx- 71

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

MyObject.Start()
    .SpecifySomeParameter(asdasd)
    .SpecifySomeOtherParameter(asdasd)
    .Execute();
Run Code Online (Sandbox Code Playgroud)

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

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

  • 仅仅是关于模式的贡献,工厂方法通常只有一个创建点,产品是基于工厂方法参数的静态选择.链创建看起来更像是一个Builder模式,你可以在其中调用不同的方法来获得结果,这些方法可以是**方法链接**中的可选方法,我们可以使用类似于'PizzaBuilder.AddSauce().AddDough()的方法. AddTopping()`more references [here](http://sourcemaking.com/design_patterns/builder) (17认同)
  • IMO,在这种情况下使用方法链接的最佳方法是创建一个要传递给函数的参数对象,如`P = MyObject.GetParamsObj().SomeParameter(asdasd).SomeOtherParameter(asdasd); Obj = MyObject.Start(); MyObject.Execute(P);`.您可以在其他调用中重新使用此参数对象,这是一个优点! (4认同)
  • 方法链接(如原始问题中所示)在违反demeter法则时被认为是错误的.请参阅:http://ifacethoughts.net/2006/03/07/the-law-of-demeter-and-object-oriented-programming/这里给出的答案实际上遵循法律,因为它是一种"建设者模式". (3认同)
  • @Marco Medrano自从我在很久以前的JavaWorld中阅读以来,PizzaBuilder的例子总是让我烦恼.我觉得我应该在我的披萨上加酱,而不是给我的厨师. (2认同)

Bri*_*kau 38

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

foo.setHeight(100).setWidth(50).setColor('#ffffff');
foo.moveTo(100,100).highlight();
Run Code Online (Sandbox Code Playgroud)

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

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

participant.getSchedule('monday').saveTo('monnday.file')
Run Code Online (Sandbox Code Playgroud)

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

  • +1提出关于安全方法链接的观点. (4认同)

Dir*_*mar 23

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

方法链接

什么时候使用它

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

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

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

  • 链接已死:( (2认同)

Tom*_*ing 20

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

怎么:

someList.addObject("str1").addObject("str2").addObject("str3")
Run Code Online (Sandbox Code Playgroud)

比任何更好:

someList.addObject("str1")
someList.addObject("str2")
someList.addObject("str3")
Run Code Online (Sandbox Code Playgroud)

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

someList = someList.addObject("str1")
someList = someList.addObject("str2")
someList = someList.addObject("str3")
Run Code Online (Sandbox Code Playgroud)

  • 让'someList'只有一次的真正好处是,给它一个更长,更具描述性的名称要容易得多.每当一个名字需要快速连续多次出现时,就会有一种趋势,即缩短它(减少重复次数并提高可读性),这使得它不那么具有描述性,损害了可读性. (24认同)
  • 它更简洁,因为即使在第一个示例中也避免使用两个'someList'部分,最终只有一行而不是三行.现在,如果这实际上好或坏取决于不同的事情,也许是一个品味的问题. (9认同)

rpr*_*ndi 8

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

我举个例子:

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

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

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

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

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

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


Joh*_*las 6

这似乎有点主观.

链接方法并不是一种本质上不好或好的imo.

可读性是最重要的.

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


abe*_*t80 6

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

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


Ape*_*ron 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
Run Code Online (Sandbox Code Playgroud)

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

  Dim loc As New HomeLocation()
  loc.X(1337)
  PrintLocation(loc)
Run Code Online (Sandbox Code Playgroud)

但这不是更容易阅读:

  PrintLocation(New HomeLocation().X(1337))
Run Code Online (Sandbox Code Playgroud)

或者,一个班级成员呢?

Public Class Dummy
   Private _locA As New Location()
   Public Sub New()
      _locA.X(1337)
   End Sub
End Class
Run Code Online (Sandbox Code Playgroud)

VS

Public Class Dummy
   Private _locC As Location = New Location().X(1337)
End Class
Run Code Online (Sandbox Code Playgroud)

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

New Dealer.CarPicker().Subaru.WRX.SixSpeed.TurboCharged.BlueExterior.GrayInterior.Leather.HeatedSeats
Run Code Online (Sandbox Code Playgroud)

和某些东西一样

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)
Run Code Online (Sandbox Code Playgroud)


不利于链接,即我不喜欢使用它

当有很多参数传递给例程时,我不使用链接,主要是因为行变得非常长,并且正如OP所提到的,当你将例程调用到其他类传递给其中一个时,它会让人感到困惑.链接方法.

还有一个例程会返回无效数据的问题,因此到目前为止,当我返回被调用的同一个实例时,我只使用了链接.正如所指出的那样,如果你在类之间进行链接,那么你会更加努力地进行调试(哪一个返回null?)并且可以增加类之间的依赖关系.

结论

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

我试着遵循这些规则.

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

  • EEEEEEWWWW !!!!!! 视觉基础!原始机器操作码比那个废话更清晰. (11认同)

Luk*_*der 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 ... ]
Run Code Online (Sandbox Code Playgroud)

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

// 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();
}
Run Code Online (Sandbox Code Playgroud)

使用这些简单的规则,您可以直接在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()
Run Code Online (Sandbox Code Playgroud)

另一个很好的例子是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 );
Run Code Online (Sandbox Code Playgroud)