【问题标题】:Method chaining - why is it a good practice, or not?方法链 - 为什么它是一个好习惯,或者不是?
【发布时间】:2009-07-09 13:44:58
【问题描述】:

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

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

这似乎被认为是一种很好的做法,因为它产生了可读的代码或“流畅的界面”。然而,对我来说,它似乎打破了面向对象本身隐含的对象调用符号 - 生成的代码并不代表对先前方法的 result 执行操作,这就是面向对象代码的方式一般预期会起作用:

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

这种差异为“调用结果对象”的点表示法创建了两种不同的含义:在链接的上下文中,上面的示例将读取为保存 participant 对象,即使该示例实际上是为了保存 getSchedule 接收到的计划对象。

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

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

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

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

旁注:我理解这个问题可以被解读为一个被伪装成问题的观点陈述。然而,事实并非如此——我真的很想了解为什么链接被认为是好的做法,以及认为它破坏了固有的面向对象表示法我哪里错了。

【问题讨论】:

  • 似乎方法链至少是用java编写智能代码的一种方式。即使不是每个人都同意..
  • 他们也称这些“流利的”方法或“流利的”接口。您可能需要更新您的标题以使用该术语。
  • 在另一个 SO 讨论中,据说流式接口是一个更大的概念,与代码可读性有关,而方法链接只是实现这一目标的一种方式。不过它们是密切相关的,所以我确实在文本中添加了标签和引用的流畅接口——我认为这些就足够了。
  • 我想到这一点的方式是,方法链接实际上是一种将缺少的功能添加到语言语法中的变通方法。 真的很糟糕如果有一个内置的替代符号 . 将忽略任何方法返回值并始终使用同一对象调用任何链式方法,则不需要 '。
  • 这是一个很棒的编码习惯,但就像所有很棒的工具一样,它也被滥用了。

标签: oop fluent-interface method-chaining


【解决方案1】:

我同意这是主观的。在大多数情况下,我避免了方法链接,但最近我也发现了一个正确的案例——我有一个方法可以接受 10 个参数,并且需要更多参数,但大多数时候你只需要指定一个很少。有了覆盖,这很快就变得非常麻烦。相反,我选择了链接方法:

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

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

关键是 - 在 99% 的情况下,如果没有方法链接,您可能会做得同样好,甚至更好。但有 1% 的人认为这是最好的方法。

【讨论】:

  • IMO,在这种情况下使用方法链接的最佳方式是创建一个参数对象以传递给函数,例如P = MyObject.GetParamsObj().SomeParameter(asdasd).SomeOtherParameter(asdasd); Obj = MyObject.Start(); MyObject.Execute(P);。您的优势是能够在其他调用中重新利用此参数对象,这是一个加号!
  • 只是我对模式的贡献,工厂方法通常只有一个创建点,产品是基于工厂方法参数的静态选择。链的创建更像是一种 Builder 模式,您可以调用不同的方法来获得结果,方法可以是可选的,如 方法链,我们可以有类似PizzaBuilder.AddSauce().AddDough().AddTopping() 更多参考here
  • 方法链接(如原始问题中的 chown)在违反 demeter 定律 时被认为是不好的。请参阅:ifacethoughts.net/2006/03/07/… 这里给出的答案实际上遵循该法则,因为它是一种“建造者模式”。
  • @Marco Medrano PizzaBuilder 的例子一直困扰着我,因为我很久以前在 JavaWorld 中读过它。我觉得我应该给我的披萨加酱汁,而不是给我的厨师。
  • 我知道你的意思,Vilx。但是当我读到list.add(someItem) 时,我读到“这段代码将someItem 添加到list 对象”。所以当我阅读PizzaBuilder.AddSauce() 时,我很自然地将其读为“这段代码正在为PizzaBuilder 对象添加调味汁”。换句话说,我将编排器(进行添加的那个)视为嵌入代码list.add(someItem)PizzaBuilder.addSauce() 的方法。但我只是觉得 PizzaBuilder 的例子有点人为。你的MyObject.SpecifySomeParameter(asdasd)例子对我来说很好。
【解决方案2】:

只要我的 2 美分;

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

我认为始终编写非常简短的行通常是一种好习惯。每一行都应该只进行一次方法调用。喜欢更多的行而不是更长的行。

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

【讨论】:

  • 换行和方法链的使用不是独立的吗?您可以按照@Vilx-answer 在换行符上链接每个调用,并且通常可以将多个单独的语句放在同一行上(例如,在 Java 中使用分号)。
  • 这个回复是完全有道理的,但它只是显示了我知道的所有调试器中存在的一个弱点,并且与问题没有具体关系。
  • @Brabster。对于某些调试器,它们可能是分开的,但是,在您调查错误时,使用中间变量进行单独调用仍然可以为您提供更丰富的信息。
  • +1,在逐步调试方法链时,您永远不知道每个方法返回什么。方法链模式是一种 hack。这是维护程序员最可怕的噩梦。
  • 我也不喜欢链接,但为什么不简单地将断点放在方法定义中?
【解决方案3】:

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

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

在我的示例中,当一个或多个链接方法将返回除 foo 之外的任何对象时,我不使用它。虽然从语法上讲,只要您为链中的对象使用正确的 API,您就可以链接任何东西,但恕我直言,更改对象会使事情变得不那么可读,并且如果不同对象的 API 有任何相似之处,可能会非常混乱。如果你在最后做了一些非常常见的方法调用(.toString().print(),无论如何)你最终会作用于哪个对象?随便阅读代码的人可能不会发现它是链中隐式返回的对象,而不是原始引用。

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

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

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

【讨论】:

  • 如果Participant 有可能没有有效的Schedule,则getSchedule 方法旨在返回Maybe(of Schedule) 类型,saveTo 方法旨在接受Maybe 类型。
【解决方案4】:

Martin Fowler 在这里进行了很好的讨论:

方法链

什么时候使用

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

方法链尤其重要 对像 parent::= 这样的语法有效 (这个|那个)*。使用不同的 方法提供了可读的方式 看看接下来会出现哪个论点。 类似的可选参数可以是 使用 Method 轻松跳过 链接。强制性条款清单, 例如 parent::= 第一秒没有 与基本形式配合得很好, 虽然它可以很好地支持 使用渐进式界面。大多数 我更喜欢嵌套函数的时间 对于这种情况。

Method最大的问题 链接是完成问题。 虽然有解决方法,但通常 如果你遇到这个你会更好 使用嵌套函数。嵌套 功能也是一个更好的选择,如果 你搞砸了 上下文变量。

【讨论】:

  • DSL 是什么意思?领域特定语言
  • @Sören:Fowler 指的是特定领域的语言。
【解决方案5】:

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

怎么样:

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

【讨论】:

  • 它更简洁,因为即使在第一个示例中您也避免使用两个“someList”部分,并以一行而不是三行结束。现在,这实际上是好是坏取决于不同的事情,也许是品味问题。
  • 只有一次'someList'的真正优势在于,给它一个更长、更具描述性的名称会容易得多。每当一个名称需要快速连续出现多次时,都会倾向于缩短名称(以减少重复并提高可读性),这会降低其描述性,损害可读性。
  • 关于可读性和 DRY 原则的好点,需要注意的是,方法链接会阻止对给定方法的返回值的自省和/或暗示每个空列表/集合和非空值的假设链中的方法(在我必须经常调试/修复的系统中导致许多 NPE 的谬误)。
  • @ChrisDodd 没听说过自动完成?
  • “如果有相同的缩进,人脑非常擅长识别文本重复”——这正是重复的问题:它使大脑专注于重复模式。因此,要阅读和理解代码,您必须强迫您的大脑超越/隐藏重复模式,以了解真正发生了什么,这是您的大脑倾向于掩盖的重复模式之间的差异。这就是为什么重复对代码可读性如此不利的原因。
【解决方案6】:

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

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

【讨论】:

    【解决方案7】:

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

    我举个例子:

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

    所以当你调用 foodStore.getLocalStore(parameters).getPriceforProduct(anything)

    您不仅像您一样依赖 FoodStore,还依赖 LocalStore。

    如果 getPriceforProduct(anything) 发生变化,您不仅需要更改 FoodStore,还需要更改调用链式方法的类。

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

    话虽如此,我个人喜欢在编写 Ruby 时链接它们。

    【讨论】:

      【解决方案8】:

      这似乎有点主观。

      方法链接在 imo 中并不是天生的好坏。

      可读性是最重要的。

      (还要考虑如果有大量的方法被链接起来会使事情变得非常脆弱)

      【讨论】:

      • 它可能确实是主观的,因此是主观标签。我希望答案能向我强调的是,在哪些情况下,方法链接将是一个好主意——现在我没有看到太多,但我认为这只是我未能理解这个概念的优点,而不是链接本身固有的不好的东西。
      • 如果导致高耦合,岂不是天生不好?将链条分解成单独的语句不会降低可读性。
      • 取决于你想变得多么教条。如果它产生了更具可读性的东西,那么在很多情况下这可能是更可取的。这种方法的最大问题是对象上的大多数方法将返回对对象本身的引用,但通常该方法将返回对子对象的引用,您可以在该子对象上链接更多方法。一旦你开始这样做,那么另一个编码员就很难解开正在发生的事情。此外,在大型复合语句中调试方法的任何功能都会很痛苦。
      【解决方案9】:

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

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

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

      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
      

      与类似的东西

      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. 在简化代码时使用它

      【讨论】:

        【解决方案10】:

        方法链可以允许直接在 Java 中设计高级DSLs。本质上,您至少可以对这些类型的 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 所做的那样。在此处查看来自my blog 的相当复杂的 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 );
        

        【讨论】:

        • @user877329:是的,它几乎可以用在任何了解接口和子类型多态性的面向对象编程语言中
        【解决方案11】:

        在大多数情况下,方法链接可能只是一个新事物,但我认为它有它的位置。在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);
        

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

        【讨论】:

        • 这是一个with方法链接的Fluent Interface示例,所以这里的UseCase略有不同。您不仅在链接,而且在创建易于阅读的内部特定领域语言。在旁注中,CI 的 ActiveRecord 不是 ActiveRecord。
        【解决方案12】:

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

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

        当别人说它损害可读性时,我真的不明白他们在说什么。它是我用过的最简洁、最有凝聚力的编程形式之一。

        还有这个:

        convertTextToVoice.LoadText("source.txt").ConvertToVoice("destination.wav");

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

        public void foo(params object[] items)

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

        【讨论】:

        • +1 on " 主要谬误是认为这是一种面向对象方法,而实际上它更像是一种函数式编程方法比什么都重要”。突出的用例是对对象进行无状态操作(而不是更改其状态,而是返回一个继续操作的新对象)。 OP 的问题和其他答案显示了有状态的动作,这些动作确实看起来很尴尬。
        • 是的,你是对的,它是一种无状态操作,除了我通常不创建新对象而是使用依赖注入来使其成为可用服务。是的,有状态的用例并不是我认为方法链接的目的。我看到的唯一例外是,如果您使用某些设置初始化 DI 服务并使用某种看门狗来监视状态,例如某种 COM 服务。恕我直言。
        【解决方案13】:

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

        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 脚本和存储过程或视图。

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

        【讨论】:

          【解决方案14】:

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

          之前:

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

          之后:

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

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

          我做出这种改变的理由

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

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

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

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

          【讨论】:

          • 但是不会为每个调用返回一个新实例会产生相当多的开销,尤其是在您使用较大的项目时?至少对于非托管语言而言。
          • @Apeiron 性能方面,return this 托管或其他方式肯定更快。我认为它以“非自然”的方式获得了流利的 api。 (添加原因 6:显示一个不流畅的替代方案,它没有开销/附加功能)
          • 我同意你的看法。最好让 DSL 的底层状态保持不变并在每次方法调用时返回新对象,而不是......我很好奇:你提到的那个库是什么?
          【解决方案15】:

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

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

          这很重要,原因与 DRY 总是很重要的原因相同;如果A出现错误,而这些操作需要在B上进行,你只需要更新1处,而不是3处。

          从务实的角度来看,在这种情况下优势很小。尽管如此,打字少一点,健壮一点(干),我会接受的。

          【讨论】:

          • 源码中重复一个变量名与DRY原则无关。 DRY 声明“每一条知识都必须在系统中具有单一的、明确的、权威的表示”,或者换句话说,避免 knowledge 的重复(而不是 text 的重复i>)。
          • 肯定是重复犯干。重复变量名(不必要地)会以与其他形式的干式相同的方式导致所有邪恶:它创建更多的依赖关系和更多的工作。在上面的例子中,如果我们重命名 A,wet 版本将需要进行 3 次更改,如果三个中的任何一个丢失,都会导致调试错误。
          • 我看不出你在这个例子中指出的问题,因为所有的方法调用都彼此接近。此外,如果您忘记在一行中更改变量的名称,编译器将返回一个错误,并且在您运行程序之前会更正此错误。此外,变量的名称仅限于其声明的范围。除非这个变量是全局的,这已经是一种不好的编程习惯。 IMO,DRY 不是减少打字,而是保持隔离。
          • “编译器”可能会返回错误,或者您使用的是 PHP 或 JS,而当您遇到这种情况时,解释器可能只会在运行时发出错误。
          【解决方案16】:

          有意见的答案

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

          一些问题:

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

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

          编译时间可能会更慢,具体取决于语言和编译器,因为表达式的解析可能要复杂得多。

          我相信与所有事情一样,链接是一个很好的解决方案,在某些情况下可以派上用场。应谨慎使用,了解其含义,并将链元素的数量限制在几个。

          【讨论】:

          • 完全同意。也使方法返回原始对象似乎只是为了使语法含糖-返回原始对象与imo方法的实际功能和责任无关。为了语法的缘故,它以牺牲单一责任/关注为代价强加了一个返回值。
          【解决方案17】:

          好的:

          1. 它很简洁,但允许您优雅地将更多内容放入一行中。
          2. 您有时可以避免使用变量,这有时可能很有用。
          3. 它的性能可能会更好。

          坏处:

          1. 您正在实现返回,本质上是向对象上的方法添加功能,而这些功能并不是这些方法的真正用途。它返回一些你已经拥有的东西,纯粹是为了节省一些字节。
          2. 当一条链指向另一条链时,它会隐藏上下文切换。您可以使用 getter 来实现这一点,除非上下文切换时非常清楚。
          3. 多行链接看起来很难看,不能很好地与缩进配合使用,并且可能会导致一些运算符处理混乱(尤其是在具有 ASI 的语言中)。
          4. 如果您想开始返回对链式方法有用的其他内容,您可能会更难修复它或遇到更多问题。
          5. 您正在将控制权卸载到一个实体,而您通常不会为了方便而将其卸载到该实体上,即使在严格类型化的语言中也无法始终检测到由此引起的错误。
          6. 它的性能可能会更差。

          一般:

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

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

          在添加时,它可能会被滥用,例如代替其他方法(例如传递数组)或以奇怪的方式混合方法(parent.setSomething().getChild().setSomething().getParent().setSomething( ))。

          【讨论】:

            【解决方案18】:

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

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

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

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

            【讨论】:

              猜你喜欢
              • 2020-01-03
              • 2010-12-04
              • 1970-01-01
              • 1970-01-01
              • 2016-12-19
              • 2020-01-25
              • 2014-05-01
              • 2019-01-11
              • 1970-01-01
              相关资源
              最近更新 更多