关于oop:方法链接 – 为什么这是一个好的做法,或不是?

Method chaining - why is it a good practice, or not?

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

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

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

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

这种差异为"调用结果对象"的点表示法创造了两种不同的含义:在链接上下文中,上面的示例将被理解为保存参与者对象,即使该示例实际上是为了保存getschedule接收的计划对象。

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

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

最后两个方法调用引用getsocialstream的结果,而前面的方法调用引用参与者。也许实际上在上下文发生变化的地方编写链是一种糟糕的实践(是吗?)但是即使这样,您也必须不断地检查看起来相似的点链实际上是否保持在相同的上下文中,或者只处理结果。

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

旁注:我理解这个问题可以理解为一个隐藏在问题上的观点陈述。然而,这并不是——我真的想理解为什么链接被认为是一种好的实践,以及我在认为它破坏了固有的面向对象的符号时哪里出错。


只有我的2美分;

方法链接使调试变得困难:-你不能把断点放在一个简洁的点上,这样你就可以把程序停在你想要的地方。-如果这些方法中有一个抛出了异常,并且您得到了一个行号,那么您就不知道"链"中的哪个方法导致了这个问题。

我认为写一些简短的句子通常是个好习惯。每行只应进行一个方法调用。比起长线,更喜欢多线。

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


我同意这是主观的。在很大程度上,我避免了方法链接,但最近我也发现了一种情况,在这种情况下,这是正确的事情——我有一种方法,它接受了类似10个参数的东西,并且需要更多的参数,但在大多数情况下,您只需要指定一些参数。有了覆盖,这变得非常麻烦,非常快。相反,我选择了链式方法:

1
2
3
4
MyObject.Start()
    .SpecifySomeParameter(asdasd)
    .SpecifySomeOtherParameter(asdasd)
    .Execute();

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

关键是-在99%的情况下,没有方法链接,您可能也可以做得更好或者更好。但是有1%的人认为这是最好的方法。


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

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

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

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

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

…不能保证(作为外部开发人员查看代码)getschedule实际上会返回有效的非空计划对象。此外,调试这种类型的代码通常要困难得多,因为许多IDE不会在调试时将方法调用作为可以检查的对象进行评估。IMO,任何时候为了调试的目的,您可能需要一个对象来检查,我宁愿将它放在一个显式变量中。


马丁·福勒在这里有一个很好的讨论:

Method Chaining

When to use it

Method Chaining can add a great deal
to the readability of an internal DSL
and as a result has become almost a
synonum for internal DSLs in some
minds. Method Chaining is best,
however, when it's used in conjunction
with other function combinations.

Method Chaining is particularly
effective with grammars like parent::=
(this | that)*. The use of different
methods provides readable way of
seeing which argument is coming next.
Similarly optional arguments can be
easily skipped over with Method
Chaining. A list of mandatory clauses,
such as parent::= first second doesn't
work so well with the basic form,
although it can be supported well by
using progressive interfaces. Most of
the time I'd prefer Nested Function
for that case.

The biggest problem for Method
Chaining is the finishing problem.
While there are workarounds, usually
if you run into this you're better off
usng a Nested Function. Nested
Function is also a better choice if
you are getting into a mess with
Context Variables.


在我看来,方法链接有点新颖。当然,它看起来很酷,但我看不出它有什么真正的优势。

如何:

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

比以下更好:

1
2
3
someList.addObject("str1")
someList.addObject("str2")
someList.addObject("str3")

当addObject()返回一个新对象时,可能会出现异常,在这种情况下,无约束的代码可能会有点麻烦,比如:

1
2
3
someList = someList.addObject("str1")
someList = someList.addObject("str2")
someList = someList.addObject("str3")


这很危险,因为您依赖的对象可能比预期的多,例如,您的调用会返回另一个类的实例:

我举个例子:

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

所以当你调用foodstore.getlocalstore(parameters.getPriceForProduct(anything)时

你不仅像你一样依赖于食品店,而且还依赖于本地商店。

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

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

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


方法链可以允许直接在Java中设计高级DSL。本质上,您至少可以为这些类型的DSL规则建模:

1
2
3
4
5
1. SINGLE-WORD
2. PARAMETERISED-WORD parameter
3. WORD1 [ OPTIONAL-WORD]
4. WORD2 { WORD-CHOICE-A | WORD-CHOICE-B }
5. WORD3 [ , WORD3 ... ]

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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,就像我创建的库Jooq所做的那样。从我的博客中看到一个相当复杂的SQL示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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文档。一个例子:

1
2
3
4
5
6
7
8
9
10
11
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 );


链接的好处
我喜欢在哪里用它

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

我知道这是人为的例子,但你说你有下列课程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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

假设您没有访问基类的权限,或者说默认值是动态的,基于时间等。是的,您可以实例化然后更改值,但这会变得很麻烦,特别是如果您只是将值传递给一个方法:

1
2
3
  Dim loc As New HomeLocation()
  loc.X(1337)
  PrintLocation(loc)

但这不是更容易阅读吗:

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

或者,一个班级成员呢?

1
2
3
4
5
6
Public Class Dummy
   Private _locA As New Location()
   Public Sub New()
      _locA.X(1337)
   End Sub
End Class

VS

1
2
3
Public Class Dummy
   Private _locC As Location = New Location().X(1337)
End Class

这就是我使用链接的方式,通常我的方法只是为了配置,所以它们只有2行长,设置一个值,然后是Return Me。对于我们来说,它已经将非常难以阅读和理解的大行清除成一行,就像一个句子。类似的东西

1
New Dealer.CarPicker().Subaru.WRX.SixSpeed.TurboCharged.BlueExterior.GrayInterior.Leather.HeatedSeats

和一些类似的

1
2
3
4
5
6
7
8
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提到的,当您将例程调用到其他类以传递给其中一个链接方法时,可能会让人困惑。

还有一个问题是,一个例程会返回无效的数据,到目前为止,我只在返回被调用的同一个实例时使用了链接。正如前面指出的那样,如果在类之间进行链接,会使调试更加困难(哪个类返回空值?)并且可以增加类之间的依赖耦合。

结论

就像生活中的所有事情和编程一样,链接既不是好的,也不是坏的。如果你能避免坏的,那么链接将是一个巨大的好处。

我试着遵守这些规则。

  • 尽量不要在类之间链接
  • 专门为链锁
  • 锁链中只做一件事常规
  • 当它提高可读性时使用它
  • 当它使代码更简单时使用它

  • 这似乎有点主观。

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

    可读性是最重要的。

    (还要考虑,如果有变化,大量方法被链接会使事物变得非常脆弱)


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

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


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

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

    我不太明白别人说的话会损害可读性。它是我使用过的最简洁和内聚的编程形式之一。

    此外:

    converttexttovoice.loadtext("source.txt").converttovoice("destination.wav");

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

    public void foo(params object[]items)

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


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

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

    这看起来比:

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

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


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

    之前:

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

    后:

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

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

    我对这一变化的理由是:

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

  • 系统库中的方法链接也以这种方式实现(如Linq或String):

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

    1
    2
    page1 = collection.limit(10);
    page2 = collection.offset(10).limit(10);
  • 复制实现还可用于构建对象:

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

    其中,setBackground(color)函数更改实例,但不返回任何内容(如它应该返回的内容)。

  • 函数的行为更容易预测(见第1点和第2点)。

  • 使用一个简短的变量名也可以减少代码混乱,而不必在模型上强制使用API。

    1
    2
    var p = participant; // create a reference
    p.addSchedule(events[1]);p.addSchedule(events[2]);p.setStatus('attending');p.save()
  • 结论:在我看来,使用return this实现的流畅接口是错误的。


    这里完全忽略了一点,那就是方法链接允许干燥。它是"with"(在某些语言中实现得不好)的有效代言人。

    1
    2
    3
    4
    5
    A.method1().method2().method3(); // one A

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

    由于同样的原因,dry总是很重要的;如果a结果是一个错误,并且这些操作需要在b上执行,那么只需要在1个位置更新,而不需要在3个位置更新。

    从实际意义上讲,这种情况下的优势很小。不过,少打一点字,再打一点(干),我就要了。


    我通常讨厌方法链接,因为我认为它会降低可读性。紧凑性常常与可读性混淆,但它们不是相同的术语。如果您在一个语句中做所有的事情,那么这是紧凑的,但是大多数情况下它比在多个语句中做的更不可读(更难理解)。正如您注意到的,除非您不能保证所用方法的返回值相同,否则方法链接将是一个混乱的来源。

    1)

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

    VS

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

    2)

    1
    2
    3
    participant
        .getSchedule('monday')
            .saveTo('monnday.file');

    VS

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

    3)

    1
    2
    3
    4
    5
    6
    participant
        .attend(event)
        .setNotifications('silent')
        .getSocialStream('twitter')
            .postStatus('Joining '+event.name)
            .follow(event.getSocialId('twitter'));

    VS

    1
    2
    3
    4
    5
    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打断,因为松耦合在该场景中更重要。


    好:

  • 它很简洁,但允许您优雅地将更多内容放在一行中。
  • 有时可以避免使用变量,这可能偶尔有用。
  • 它可能表现得更好。
  • 坏处:

  • 您正在实现返回,本质上是为对象上的方法添加功能,而这些对象实际上并不是这些方法要做的工作的一部分。它返回一些您已经拥有的东西,纯粹是为了节省几个字节。
  • 当一个链指向另一个链时,它隐藏上下文切换。你可以通过getter得到这个,除非上下文切换时非常清楚。
  • 多行链接看起来很难看,不能很好地处理缩进,可能会导致一些操作员处理混乱(特别是在使用ASI的语言中)。
  • 如果您想开始返回对链接方法有用的其他内容,那么修复它或使用它遇到更多问题可能会比较困难。
  • 您正在将控制权卸载到一个通常不会纯粹为了方便而卸载到的实体上,即使是在严格类型化的语言中,也不能总是检测到由此导致的错误。
  • 可能表现更差。
  • 一般:

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

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

    在访问时,它可能会被误用,例如代替另一种方法(例如传递数组)或以奇怪的方式混合方法(parent.setsomehing().getchild().setsomehing().getparent().setsomehing())。