代码之家  ›  专栏  ›  技术社区  ›  Ilari Kajaste

方法链接-为什么这是一个好的实践,或不是?

  •  134
  • Ilari Kajaste  · 技术社区  · 15 年前

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

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

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

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

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

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

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

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

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

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

    16 回复  |  直到 6 年前
        1
  •  71
  •   Vilx-    15 年前

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

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

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

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

        2
  •  72
  •   RAY    10 年前

    只有我的2美分;

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

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

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

        3
  •  36
  •   Brian Moeskau    10 年前

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

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

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

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

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

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

        4
  •  22
  •   Dirk Vollmar    15 年前

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

    Method Chaining

    何时使用

    方法链接可以增加很多 内部DSL的可读性 结果几乎变成了 一些内部DSL的SynoNum 头脑。方法链接最好, 但是,当它与 其他功能组合。

    尤其是方法链 对诸如parent::之类的语法有效= (这个那个)*。不同的用法 方法提供了 看看接下来会发生什么争论。 类似地,可选参数可以是 使用方法很容易跳过 链接。强制性条款清单, 例如父级::=第一秒不 与基本形式配合得很好, 尽管它能得到很好的支持 使用渐进式接口。大部分 我更喜欢嵌套函数的时间 那样的话。

    方法的最大问题 链接是最终的问题。 当有解决办法时,通常 如果你遇到这个,你会好起来的 usng嵌套函数。嵌套的 如果 你正在搞得一团糟 上下文变量。

        5
  •  20
  •   Tom Dalling    15 年前

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

    如何:

    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")
    
        6
  •  8
  •   rprandi    15 年前

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

    我举个例子:

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

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

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

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

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

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

        7
  •  6
  •   John Nicholas    15 年前

    这似乎有点主观。

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

    可读性是最重要的。

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

        8
  •  6
  •   aberrant80    15 年前

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

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

        9
  •  6
  •   Apeiron    14 年前

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

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

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

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

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

    结论

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

    我试着遵守这些规则。

    1. 尽量不要在类之间链接
    2. 专门为 链锁
    3. 锁链中只做一件事 常规
    4. 当它提高可读性时使用它
    5. 当它使代码更简单时使用它
        10
  •  6
  •   Lukas Eder    13 年前

    方法链接允许设计高级 DSLs 在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示例 my blog 在这里:

    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 );
    
        11
  •  3
  •   Nathan    15 年前

    方法链接对于大多数情况来说可能只是一种新颖的方法,但我认为它有它的位置。可以在中找到一个示例 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);
    

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

        12
  •  2
  •   Bob Fanger    13 年前

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

    之前:

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

    后:

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

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

    我对这一变化的推理 :

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

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

    结论:
    在我看来,一个流畅的界面 归还这个 实现是错误的。

        13
  •  1
  •   Shane Thorndike    7 年前

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

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

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

    此外:

    converttexttovoice.loadtext(“source.txt”).converttovoice(“destination.wav”);

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

    public void foo(params object[]items)

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

        14
  •  0
  •   Ilari Kajaste    13 年前

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

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

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

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

        15
  •  0
  •   jgmjgm    7 年前

    好:

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

    坏处:

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

    一般:

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

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

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

        16
  •  0
  •   inf3rno    6 年前

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

    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脚本、存储过程或视图在多个语句中编写代码的人,最终也会失败。

    至于枯燥的解释:“避免知识的重复(不是文本的重复)。”和“少打字,甚至不重复文本。”,第一个原则是什么意思,但第二个原则是常见的误解,因为许多人不能理解过于复杂的胡说八道,如“每一个知识必须有一个单一的,不可重复的系统中的模糊、权威的表示”。第二种是不惜一切代价的紧凑性,在这种情况下会中断,因为它会降低可读性。当您在有界上下文之间复制代码时,第一个解释会被DDD打断,因为松耦合在该场景中更重要。