什么时候去流利的C#?


78

在很多方面,我真的很喜欢Fluent接口的概念,但是使用C#的所有现代功能(初始化程序,lambda和命名参数),我发现自己在思考:“值得吗?”,“这是正确的模式吗?采用?”。任何人都可以给我(如果不是被接受的做法),至少是他们自己的经验或决策矩阵,以便何时使用Fluent模式?

结论:

到目前为止的答案有一些好的经验法则:

  • 当操作员多于设置员时,流利的接口将提供极大帮助,因为调用从上下文传递中受益更多。
  • 流利的接口应该被认为是api之上的一层,而不是唯一的使用方法。
  • lambda,初始值设定项和命名参数等现代功能可以配合使用,使流畅的界面更加友好。

这是一个使我觉得不需要的现代功能的示例。以一个(也许很差的例子)Fluent接口为例,该接口允许我创建一个Employee,例如:

Employees.CreateNew().WithFirstName("Peter")
                     .WithLastName("Gibbons")
                     .WithManager()
                          .WithFirstName("Bill")
                          .WithLastName("Lumbergh")
                          .WithTitle("Manager")
                          .WithDepartment("Y2K");

可以很容易地用初始化器编写,例如:

Employees.Add(new Employee()
              {
                  FirstName = "Peter",
                  LastName = "Gibbons",
                  Manager = new Employee()
                            {
                                 FirstName = "Bill",
                                 LastName = "Lumbergh",
                                 Title = "Manager",
                                 Department = "Y2K"
                            }
              });

在本示例中,我还可以在构造函数中使用命名参数。


1
好问题,但我认为它更像是一个维基问题
Ivo

您的问题被标记为“流利的休眠”。那么,您是要决定是否创建一个流畅的接口,还是要使用流畅的nhibernate与XML配置?
Ilya Kogan

1
投票决定迁移到Programmers.SE
Matt Ellen

@Ilya Kogan,我认为它实际上被标记为“ fluent-interface”,这是流利的接口模式的通用标记。这个问题与nhibernate无关,而是您仅说是否创建一个流畅的界面。谢谢。

1
这篇文章激发了我思考在C语言中使用这种模式的方法。我的尝试可以在Code Review姐妹网站上找到
otto

Answers:


28

编写一个流畅的界面(我已经涉足了它)需要付出更多的努力,但是它确实有回报,因为如果您做对的话,那么生成的用户代码的意图就更加明显。它本质上是一种特定于域的语言。

换句话说,如果您读取的代码比编写的代码多得多(什么代码不是?),那么您应该考虑创建一个流畅的界面。

流利的接口更多地是关于上下文,而不仅仅是配置对象的方法。如您在上面的链接中所见,我使用了流利的API来实现:

  1. 上下文(因此,当您通常在同一序列中按顺序执行许多操作时,可以链接这些操作,而不必一遍又一遍地声明上下文)。
  2. 可发现性(当您进入时,objectA.intellisense会给您很多提示。在我上面的例子中,plm.Led.为您提供了控制内置LED的所有选项,并plm.Network.为您提供了可以通过网络接口进行操作的所有内容。 plm.Network.X10.为您提供了X10设备的网络操作。构造函数初始化程序不会提供此功能(除非您想为每种不同类型的操作构造一个对象,这不是惯用的)。
  3. 反射(在上面的示例中未使用)-能够接受传入的LINQ表达式并对其进行操作是一种非常强大的工具,尤其是在我为单元测试构建的一些辅助API中。我可以传递属性获取器表达式,构建一堆有用的表达式,编译和运行这些表达式,甚至可以使用属性获取器来设置我的上下文。

我通常要做的一件事是:

test.Property(t => t.SomeProperty)
    .InitializedTo(string.Empty)
    .CantBeNull() // tries to set to null and Asserts ArgumentNullException
    .YaddaYadda();

如果没有流畅的界面,我看不到如何做类似的事情。

编辑2:您还可以进行非常有趣的可读性改进,例如:

test.ListProperty(t => t.MyList)
    .ShouldHave(18).Items()
    .AndThenAfter(t => testAddingItemToList(t))
    .ShouldHave(19).Items();

感谢您的答复,但是我知道使用Fluent的原因,但是正在寻找更具体的理由在上面的新示例中使用它。
Andrew Hanlon

感谢您的答复。我认为您已经概述了两个良好的经验法则:1)当您有许多受益于上下文“传递”的呼叫时,请使用Fluent。2)当您的电话数量超过设定者时,请考虑Fluent。
安德鲁·汉隆

2
@ach,在此回复中我没有看到任何有关“来电多于设置者”的信息。您是否对他关于“代码读得比编写的要多得多”的说法感到困惑?这与财产获取者/财产设定者无关,而是与人阅读代码与人编写代码有关–使代码易于人类阅读,因为我们通常阅读给定代码行的频率比修改代码的频率高。
乔·怀特

@乔·怀特(Joe White),我也许应该将我的术语“呼吁”改称为“行动”。但是这个想法仍然存在。谢谢。
Andrew Hanlon

测试的反思是邪恶的!
Adronius '16

24

斯科特· 汉瑟曼( Scott Hanselman)在与乔纳森·卡特(Jonathan Carter)的播客《汉瑟尔 · 分钟》中的第260集中谈到了这一点。他们解释说,流畅的界面更像是API上的UI。您不应该提供一个流畅的接口作为唯一的访问点,而应该在“常规API接口”之上以某种代码UI的形式提供它。

乔纳森·卡特(Jonathan Carter)在他的博客中还谈到了API设计。


非常感谢您提供的信息链接,API顶部的UI是一种很好的查看方式。
安德鲁·汉隆

14

流利的接口是非常强大的功能,可以在代码上下文中提供“正确”的推理。

如果您的目标只是简单地将大量的单行代码链创建为一种伪黑匣子,那么您可能就在树错了树。另一方面,如果您使用它通过提供一种链接方法调用并提高代码可读性的方法来增加API接口的价值,那么通过大量的计划和努力,我认为付出的努力是值得的。

在创建流利的接口时,我会避免遵循似乎成为通用“模式”的方式,在其中您将所有流利的方法都命名为“ with”,因为这样会抢占其上下文的潜在的良好API接口,并因此剥夺其内在价值。

关键是将流利的语法视为特定于域的语言的特定实现。作为我在说的一个很好的例子,请看StoryQ,它使用流利度作为一种非常有价值和灵活的方式来表达DSL的方法。


感谢您的答复,做出深思熟虑的答复永远不会太晚。
安德鲁·汉隆

我不介意方法的“ with”前缀。它将它们与不返回链接对象的其他方法区分开。例如,position.withX(5)vsposition.getDistanceToOrigin()
LegendLength

5

初始说明:我对问题中的一个假设表示怀疑,并据此得出我的具体结论(在本文末尾)。因为这可能无法给出完整的涵盖性答案,所以我将其标记为CW。

Employees.CreateNew().WithFirstName("Peter")…

可以很容易地用初始化器编写,例如:

Employees.Add(new Employee() { FirstName = "Peter",  });

在我眼中,这两个版本应该表示不同的意思。

  • 与非流利版本不同,流利版本掩盖了新Employee内容也Add被收集到集合中的事实Employees-它仅表示新对象是Created。

  • 的含义….WithX(…)含糊不清,尤其是对于来自F#的人们,F#具有with对象表达式的关键字:他们可能会将其解释obj.WithX(x)为从其派生的对象objobj除了其X属性为,其属性与相同x。另一方面,对于第二个版本,很明显没有创建派生对象,并且为原始对象指定了所有属性。

….WithManager().With
  • ….With…还有另一种含义:将属性初始化的“焦点”切换到另一个对象。流利的API具有两种不同含义的事实With使得难以正确解释正在发生的事情……这也许就是为什么您在示例中使用缩进来演示该代码的预期含义的原因。这样会更清楚:

    (employee).WithManager(Managers.CreateNew().WithFirstName("Bill").…)
    //                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                     value of the `Manager` property appears inside the parentheses,
    //                     like with `WithName`, where the `Name` is also inside the parentheses 
    

结论:new T { X = x }使用流利的API(Ts.CreateNew().WithX(x))可以“隐藏”足够简单的语言功能,但是:

  1. 必须注意,所产生的流利代码的读者仍应了解其确切功能。就是说,流畅的API的含义应该透明且明确。设计这样的API可能比预期的工作要多(可能必须对其进行测试以确保易用性和接受性),和/或…

  2. 设计它可能比必要的工作还要多:在此示例中,与基本API(一种语言功能)相比,流畅的API几乎没有增加“用户舒适度”。可以说流利的API应该使底层API /语言功能“更易于使用”;也就是说,它可以节省程序员大量的精力。如果这只是写同一件事的另一种方式,那可能不值得,因为这不会使程序员的生活变得更轻松,而只会使设计人员的工作更加艰辛(请参见上面的结论1)。

  3. 以上两点都默默地认为,流畅的API是现有API或语言功能之上的一层。这个假设可能是另一个很好的准则:流利的API可能是做某事的一种额外方法,而不是唯一的方法。也就是说,提供流利的API作为“选择加入”的选择可能是一个好主意。


1
感谢您抽出宝贵的时间来回答我的问题。我承认我所选择的例子没有经过深思熟虑。当时,我实际上正在寻找将流体接口用于我正在开发的查询API。我简化了。感谢您指出错误并给出了很好的结论。
安德鲁·汉隆

2

我喜欢流利的风格,表达得很清楚。对于您所拥有的对象初始化器示例,必须具有公共属性设置器才能使用该语法,而您不会使用流畅的样式。如此说来,在您的示例中,您不会在公共设置器上获得太多收益,因为您几乎已经采用了Java风格的set / get方法风格。

这使我想起第二点,我不确定是否会以您拥有的方式使用流利的样式,并且有很多属性设置器,所以我可能会使用第二个版本,当您使用时我会觉得更好有很多动词可以链接在一起,或者至少有很多事情要做,而不是设置。


感谢您的答复,我认为您表达了一个很好的经验法则:流利的方法更好,可以通过许多设置员进行多次调用。
Andrew Hanlon

1

我对流畅接口一词并不熟悉,但是它使我想起了我使用的包括LINQ在内的几个API 。

我个人没有看到C#的现代功能如何阻止这种方法的实用性。我宁愿说他们齐头并进。例如,使用扩展方法来实现这样的接口更加容易。

也许用一个具体的例子来阐明您的答案,该例子说明了如何使用您提到的现代功能之一替换流畅的界面。


1
感谢您的答复-我添加了一个基本示例来帮助阐明我的问题。
安德鲁·汉隆
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.