方法链接——为什么它是一个好的实践,或者不是?

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

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

这似乎被认为是一个很好的实践,因为它产生了可读的代码,或者说是一个“流畅的接口”。然而,对我来说,它似乎打破了面向对象本身所隐含的对象调用表示法——产生的代码并不代表对之前方法的 结果执行操作,而面向对象的代码通常就是这样工作的:

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

这种差异为点符号“调用结果对象”创建了两种不同的含义: 在链接的上下文中,上面的示例将被理解为保存 参与者对象,即使这个示例实际上是为了保存 getScheme 接收到的调度对象。

我理解这里的区别在于是否应该期望被调用的方法返回某些内容(在这种情况下,它将返回被调用的对象本身以进行链接)。但是这两种情况并不能与符号本身区分开来,只能从被调用方法的语义上区分开来。当没有使用方法链接时,我总能知道一个方法调用操作的是与前一个调用的 结果相关的东西——通过链接,这个假设被打破了,我必须从语义上处理整个链接来理解实际被调用的对象到底是什么。例如:

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

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

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

旁注: 我理解这个问题可以被理解为一个隐藏在问题背后的意见陈述。然而,它不是——我真的想知道为什么链接被认为是一种好的实践,我在哪里错误地认为它打破了固有的面向对象符号。

88997 次浏览

这看起来有点主观。

方法链接不是固有的坏或好的 imo。

可读性是最重要的。

(还要考虑到,如果有什么变化,拥有大量的链式方法会使事情变得非常脆弱)

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

怎么做:

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年里,我对这个问题的看法发生了变化。对于可变对象,我仍然没有看到很多好处,尽管它对于避免一点点复制是有用的。但是现在我更喜欢不可变性,方法链接是我更喜欢的非破坏性更新方式,我一直在使用这种方式。

我同意这是主观的。在大多数情况下,我避免方法链接,但最近我也发现一个情况下,它是正确的事情-我有一个方法,接受10个参数,并需要更多,但在大多数时间,你只需要指定几个。随着重写,这变得非常麻烦,非常快。相反,我选择了链接方式:

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

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

关键在于——在99% 的情况下,不使用方法链接也可以做得很好甚至更好。但是有1% 的人认为这是最好的方法。

马丁•福勒(Martin Fowler)在这里进行了很好的讨论:

方法链接

什么时候用

方法链接可以添加大量内容 内部 DSL 的可读性 结果几乎变成了 一些内部 DSL 的同义词 方法链接是最好的, 然而,当它被用于联合 与其他功能组合。

方法链接是特别的 有效的语法,如: : = (this | that) * 方法提供了可读的方式 看看接下来会发生什么。 类似的可选参数可以是 方法很容易跳过 链条,一份强制性条款的清单, 第一秒没有 和基本形式配合得很好, 虽然它可以很好地支持 使用渐进式接口 我更喜欢嵌套函数 为了那个案子。

方法的最大问题 链接是最后的问题。 通常还是有变通方法的 如果你遇到这种情况,你会过得更好 使用嵌套函数 函数也是一个更好的选择,如果 你惹上麻烦了 上下文变量。

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

我举个例子:

FooStore 是一个由你拥有的许多食品商店组成的对象。 GetLocalStore ()返回一个对象,该对象保存与参数最接近的存储的信息。GetPriceforProduct (anything)是该对象的一个方法。

因此,当您调用 fooStore.getLocalStore (参数) . getPriceforProduct (anything)时

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

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

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

也就是说,我个人喜欢在编写 Ruby 时将它们链接起来。

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

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

对于大多数情况来说,方法链接可能只是一个新鲜事物,但我认为它有它的地位。在 CodeIgniter 的活动记录使用中可以找到一个例子:

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

这看起来比下面的要干净得多(在我看来,也更有道理) :

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

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

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

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

在我的示例中,当一个或多个链式方法返回除 foo 以外的任何对象时,我不使用它。虽然从语法上讲,只要在链中为该对象使用正确的 API,就可以链接任何内容,但是更改对象 IMHO 会降低可读性,并且如果不同对象的 API 有任何相似之处,那么可能会造成真正的混淆。如果在最后执行一些非常常见的方法调用(.toString().print()等等) ,那么最终将对哪个对象执行操作?随意阅读代码的人可能没有发现它是链中隐式返回的对象,而不是原始引用。

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

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

... 不能保证(作为一个外部开发人员查看代码) getScheme 实际上会返回一个有效的、非空的调度对象。另外,调试这种类型的代码通常要困难得多,因为许多 IDE 不会在调试时将方法调用作为可以检查的对象进行计算。IMO,任何时候你可能需要一个对象来检查调试的目的,我更喜欢有一个明确的变量。

只有我的两分钱

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

我认为总是写出非常简洁明了的句子是一个很好的实践。每一行应该只调用一个方法。比起长线,我更喜欢多线。

编辑: 注释中提到方法链接和换行是分开的。那倒是真的。但是,根据调试器的不同,可以在语句中间放置断点,也可以不放置断点。即使可以,使用带有中间变量的单独行也可以提供更大的灵活性,并且可以在“监视”窗口中检查大量的值,这有助于调试过程。

链接的好处
就是我喜欢用的地方

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

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

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

V 大概是

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

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

以前:

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

之后:

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

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

我对这一变化的理由 :

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

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

    myText = myText.trim().toUpperCase();
    
  3. The original object remains intact, allowing the API user to decide what to do with it. It allows for:

    page1 = collection.limit(10);
    page2 = collection.offset(10).limit(10);
    
  4. A copy implementation can also be used for building objects:

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

Conclusion:
In my opinion a fluent interface that uses an return this implementation is just wrong.

方法链接允许直接在 Java 中设计高级 领域特定语言。实际上,您至少可以建模这些类型的 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,一个用于直接在 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 );

这里完全忽略了一点,即方法链允许 返回文章页面【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】【一分钟科普】。它是“ with”(在某些语言中实现得很糟糕)的有效替代品。

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


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

这与 DRY 总是重要的原因是一样的; 如果 A 是一个错误,并且这些操作需要在 B 上执行,那么您只需要在1处进行更新,而不需要在3处进行更新。

实际上,这种情况的优势很小。不过,少一点打字,多一点健壮(DRY) ,我就接受了。

好处:

  1. 它很简洁,但是允许你优雅地将更多的东西放入一条线中。
  2. 您有时可以避免使用变量,这可能偶尔是有用的。
  3. 它可能会表现得更好。

坏处:

  1. 您正在实现返回,实际上是向对象上的方法添加功能,而这些功能并不是这些方法的本意所在。它返回一些你已经有的东西,只是为了节省几个字节。
  2. 当一个链接到另一个链接时,它隐藏上下文切换。您可以使用 getter 来获得这个结果,但是当上下文切换时就很清楚了。
  3. 多行链接看起来很丑陋,不能很好地处理缩进,并且可能导致一些操作符处理混乱(特别是在使用 ASI 的语言中)。
  4. 如果您想开始返回对链式方法有用的其他内容,那么您可能很难修复它,或者遇到更多的问题。
  5. 您正在将控制权转移给一个实体,而这个实体通常不会纯粹为了方便而转移控制权,即使在严格类型的语言中,由此造成的错误也不总是能够被检测到。
  6. 可能会更糟。

一般情况:

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

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

在 accasation 中,它可能会被误用,比如不使用另一种方法(例如传递数组) ,或者以奇怪的方式混合使用方法(Parent.setSomething ())。GetChild ().SetSomething ().GetParent ().SetSomething ().

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

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

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

还有这个:

LoadText (“ source. txt”) . ConvertToVoice (“ destination.wav”) ;

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

Public void foo (params object [] item)

并根据类型强制转换对象,或者根据用例只使用数据类型数组或集合。

我通常讨厌方法链接,因为我认为它会降低可读性。紧凑性常常与可读性混淆,但它们不是同一个术语。如果您在一个语句中执行所有操作,那么这是紧凑的,但大多数情况下,与在多个语句中执行相比,它的可读性较差(难以遵循)。正如您所注意到的,除非您不能保证所使用的方法的返回值是相同的,否则方法链接将成为混淆的根源。

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 脚本和存储过程或视图来编写代码。

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

固执己见的回答

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

一些问题:

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

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

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

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

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

import Participant
import Schedule


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

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

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

Linq 查询是方法链接的一个很好的例子,典型的查询如下所示:

lstObjects
.Where(...)
.Select(...)
.OrderBy(...)
.ThenBy(...)
.ToList();

在我看来,这是相当直观的,并避免了不必要的临时变量,以存储部分结果,人们可能几乎不感兴趣。

需要注意的一个微妙的事情是“ ThenBy”扩展方法的用法,只有在调用“ OrderBy”或“ OrderByDesending”方法之后才能调用该方法。这意味着这里还维护一个内部状态,它决定是否可以调用 ThenBy。和这个查询一样,在某些情况下,客户机应用程序可能对将内部状态存储在临时变量中不感兴趣,而只对最终结果感兴趣。

因此,在编写库时,如果我们希望提供一个 API,使其能够按照一定的顺序执行一组特定的操作,那么允许方法链接将使库的使用更加直观。