Delphi中的流畅接口

32

在Delphi中使用流畅接口的优缺点是什么?

流畅接口应该增加可读性,但我对包含许多链接方法的一个长LOC持怀疑态度。

是否存在编译器问题?
是否存在调试问题?
是否存在运行时/错误处理问题?

流畅接口用于TStringBuilder, THTMLWriterTGpFluentXMLBuilder等。


更新:
David Heffernan问我关心哪些问题。 我思考了一下,总体问题是“明确指定如何完成”与“让编译器决定如何完成”的差异。
据我所知,并没有关于编译器如何处理链式方法的文档,也没有任何规范说明编译器应该如何处理链式方法。
这篇文章中,我们可以了解到编译器如何向声明为函数的方法添加两个额外的var参数,并且标准调用约定将三个参数放在寄存器中,下一个参数放在堆栈中。因此,具有2个参数的“流畅函数方法”将使用堆栈,而具有2个参数的“普通过程方法”仅使用寄存器。
我们还知道编译器会对二进制代码进行一些优化(例如作为函数结果的字符串, 评估顺序, 本地过程引用),但有时会对程序员产生意想不到的副作用。
因此,内存/堆栈/寄存器管理更加复杂,编译器可能会做出一些带有意外副作用的魔法,这对我来说很可疑。 因此有这个问题。

读完答案(非常好的答案)后,我的担忧大大减轻,但我仍然偏爱原来的选择 :)


2
我从学习那篇文章中的Delphi示例中得出的结论是,我有一种阅读名为“停止思考Java”的书的冲动 :-) - Free Consulting
2
有些人喜欢它,有些人不喜欢。你怎么看? - David Heffernan
1
我没有看到任何安全问题。这只是关于风格的问题。我不知道builder-DP是什么! - David Heffernan
http://en.wikipedia.org/wiki/Builder_pattern - Jørn E. Angeltveit
@David:例如这样:TMyClass.Create.Random(SomeGlobalVar).Double(SomeGlobalVar).Free; 今天和将来是否等同于 m := TMyClass.Create; m.Random(SomeGlobalVar); m.Double(SomeGlobalVar); m.Free;?(愚蠢的例子 - 我知道) - Jørn E. Angeltveit
显示剩余6条评论
5个回答

24

大家都在写负面问题,那我们来强调一些积极的问题。唔,唯一的积极问题就是——输入量减少了(在某些情况下大大减少)。

我写GpFluentXMLBuilder只是因为我讨厌在创建XML文档时打入大量代码。没有更多,也没有更少。

流畅接口的好处是,如果你不喜欢这个习惯用法,你不必按照流畅方式使用它们。它们可以完全以传统方式使用。

编辑:一个“简短和易读性”的观点。

我正在调试一些旧代码,偶然发现了这个:

fdsUnreportedMessages.Add(CreateFluentXml
  .UTF8
  .AddChild('LogEntry')
    .AddChild('Time', Now)
    .AddSibling('Severity', msg.MsgID)
    .AddSibling('Message', msg.MsgData.AsString)
  .AsString);

我立刻知道这段代码是在干什么。然而,如果代码看起来像这样(我并不断言这段代码能否通过编译,我只是为了演示拼凑在一起的):

var
  xmlData: IXMLNode;
  xmlDoc : IXMLDocument;
  xmlKey : IXMLNode;
  xmlRoot: IXMLNode;

  xmlDoc := CreateXMLDoc;
  xmlDoc.AppendChild(xmlDoc.CreateProcessingInstruction('xml', 
    'version="1.0" encoding="UTF-8"'));
  xmlRoot := xmlDoc.CreateElement('LogEntry');
  xmlDoc.AppendChild(xmlRoot);
  xmlKey := xmlDoc.CreateElement('Time');
  xmlDoc.AppendChild(xmlKey);
  xmlData := xmlDoc.CreateTextNode(FormatDateTime(
    'yyyy-mm-dd"T"hh":"mm":"ss.zzz', Now));
  xmlKey.AppendChild(xmlData);
  xmlKey := xmlDoc.CreateElement('Severity');
  xmlDoc.AppendChild(xmlKey);
  xmlData := xmlDoc.CreateTextNode(IntToStr(msg.MsgID));
  xmlKey.AppendChild(xmlData);
  xmlKey := xmlDoc.CreateElement('Message');
  xmlDoc.AppendChild(xmlKey);
  xmlData := xmlDoc.CreateTextNode(msg.MsgData.AsString);
  xmlKey.AppendChild(xmlData);
  fdsUnreportedMessages.Add(xmlKey.XML);

我需要相当长的时间(和一杯咖啡)来理解它的作用。

编辑2:

Eric Grange在评论中提出了一个非常合理的观点。实际上,人们会使用一些XML封装程序而不是直接使用DOM。例如,使用OmniXML包中的OmniXMLUtils,代码将如下所示:

var
  xmlDoc: IXMLDocument;
  xmlLog: IXMLNode;

  xmlDoc := CreateXMLDoc;
  xmlDoc.AppendChild(xmlDoc.CreateProcessingInstruction(
    'xml', 'version="1.0" encoding="UTF-8"'));
  xmlLog := EnsureNode(xmlDoc, 'LogEntry');
  SetNodeTextDateTime(xmlLog, 'Time', Now);
  SetNodeTextInt(xmlLog, 'Severity', msg.MsgID);
  SetNodeText(xmlLog, 'Message', msg.MsgData.AsString);
  fdsUnreportedMessages.Add(XMLSaveToString(xmlDoc));

然而,我更喜欢流畅的版本。[我从不使用代码格式化程序。]



2
@gabr,我不会给你的答案投反对票,因为从技术上来说是正确的(输入较少)。然而,你可以通过使用with poorly (with MyQuery, MyButton, MyTreeView, MyOtherThing do...)更少地输入。虽然你能省下很多按键,但这基本上会使代码变得极其难以调试和维护。我认为流畅接口可能会遇到相同的问题。 - Ken White
2
+1. 别忘了已保存的本地变量。听起来不像什么,但我在运行时填充列表时,大多数情况下都使用流畅接口,并使用不同的类。如果没有流畅式接口,即使您只需要设置一个属性的值,也会为每个类类型需要使用一个变量,这样就会出现问题。 - Cosmin Prund
7
在XML的情况下,流畅的接口实际上更容易从代码中看到期望的结果文档。我无法表达使用这种方法创建具有复杂结构的文档有多么容易。而且,由于你可以清楚地“看到”输出,因此更容易维护——例如,在特定位置添加一个新元素。 - Leonardo Herrera
1
@Leonardo,说得好!是的,使用流畅接口时代码更加“明显”。 - gabr
2
@gabr,我得再看看GpFluentXMLBuilder。第一次看时我并不这么认为,但你可能是对的(@Leonardo的观点似乎也很好)。谢谢。 - Ken White
显示剩余8条评论

18

编译问题:

如果你使用接口(而不是对象),每次链中的调用都会产生引用计数开销,即使每次返回相同的接口,编译器也无法知道。因此,你将生成更大的代码,具有更复杂的堆栈。

调试问题:

由于调用链被视为单个指令,因此无法在中间步骤上单步执行或设置断点。你也无法在中间步骤评估状态。 调试中间步骤的唯一方法是在汇编视图中跟踪。 如果同一个方法在流畅的链中多次发生,则调试器中的调用堆栈也将不清晰。

运行时问题:

当使用接口连接链时(而不是对象),你必须为引用计数开销付费,并且需要更复杂的异常帧。 在流畅的链中,你不能使用try..finally结构,因此不能保证关闭在流畅的链中打开的内容。

调试/错误日志问题:

异常及其堆栈跟踪将把链视为单个指令,因此如果你在.DoSomething中崩溃,而调用链中有几个.DoSomething调用,你将不知道是哪个调用引起了问题。

代码格式问题:

据我所知,现有的代码格式化程序都无法正确布局流畅的调用链,因此只能手动格式化以保持可读性。如果运行自动格式化程序,则通常会将链转换为难以阅读的混乱格式。


2
你可以单步执行表达式,如果你在每个过程结束时使用F11跳出。虽然这样非常耗时,因此不建议使用。 - Jeroen Wiert Pluimers

7

是否存在编译问题?

没有。

是否存在调试问题?

有。 由于所有链接的方法调用被视为一个表达式,即使您像您所链接到的维基百科示例中那样将它们写在多行中,调试时也会出现问题,因为您无法逐步执行它们。

是否存在运行时/错误处理问题?

编辑这是我编写的一个测试控制台应用程序,用于测试使用流畅接口的实际运行时开销。我为每次迭代分配了6个属性(实际上是相同的2个值各3次)。结论是:

  • 使用接口:运行时增加70%,取决于设置的属性数量。仅设置两个属性时开销较小。
  • 使用对象:使用流畅接口会更
  • 未测试记录。它不能很好地与记录一起使用!

我个人不介意那些“流畅接口”。以前从未听说过这个名字,但我已经在其中使用了,特别是在从代码中填充列表的代码中。 (有点像您发布的XML示例)。我不认为它们难以阅读,特别是如果一个人熟悉这种编码方式并且方法名称有意义。至于那一长行代码,请看维基百科的示例:您不需要把它全部放在一行代码上。

我清楚地记得在Turbo Pascal中使用它们来初始化屏幕,这可能是我不介意它们并且有时也使用它们的原因。不幸的是,谷歌让我失望了,我找不到任何旧TP代码的代码样例。


很有趣的是,当使用对象实现时,它们甚至比经典的逐行代码更快,感谢您运行测试。 - mjn

3
我对使用 "流畅接口" 的好处表示怀疑。
据我所见,这样做的重点是允许您避免声明变量。因此,带来了同样的问题,就像可怕的 With 语句一样(请参见其他答案)。
老实说,我从未理解使用 With 语句的动机,我也不理解使用流畅接口的动机。我的意思是,定义一个变量有那么难吗? 所有这些胡言乱语只是为了懒惰。
我认为,与其增加可读性(乍一看似乎确实可以通过输入/阅读更少的内容),它实际上会使代码更加模糊。
所以,我再次问:为什么要首选使用流畅接口?
它是由 Martin Fowler 提出的,所以肯定很酷?不,我不相信这个。

6
“所有这些废话只是为了让懒惰变得更容易。”- 只是想指出,关于汇编语言、第三代编程语言、集成开发环境、表单设计器、生成式编程、垃圾收集和自动重构工具等方面也曾有过同样的说法。 - Kenneth Cochran

2

这是一种只写不读的注释,如果没有查看所有相关方法的文档,很难理解。此外,这种注释与Delphi和C#属性不兼容 - 如果需要设置属性,则需要回滚到使用常规注释,因为无法链接属性赋值。


这是一种只写不读的记号,如果没有查看所有相关方法的文档,很难理解。这是一种夸张的说法。在正确的情况下使用时,它是相当可读的。 - Shannon Matthews

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接