‘with command’ 和 ‘fluent interface’ 在Delphi中有什么区别?

huangapple go评论84阅读模式
英文:

What's the difference between 'with command' and 'fluent interface' in Delphi?

问题

为什么我应该在Delphi中使用流畅接口而不是使用'with命令'?

我听说过两者,但我还没有找到区别。我正试图为我的项目找到最佳选项。它似乎以相同的方式工作,只是有一些语法差异。

英文:

Why should I use fluent interface in Delphi instead of using 'with command'?

I heard about both, but I haven't found the difference. I'm trying to find the best one for my project. ​It seems to work in the same way, just with some grammar differences.

答案1

得分: 1

以下是您要求的翻译内容:

有一些(非常少的)用例,with 是“安全”的,但在其他情况下应该避免使用。

因此,我特别不打算详细讨论您应该或不应该使用 with 的原因,而是专注回答您问题的第二部分:with 和流畅API 似乎直接相关,但实际上并不是。

with

with 接受一个或多个符号,并创建一个范围,在该范围中,这些符号优先于包含范围中具有相同名称的其他符号:

所以,如果我们想象我们正在为某种形式编写代码,并且需要设置该形式上某个组件的多个属性,其中一些属性也存在于该形式类本身(即具有相同的名称),我们可以使用 with 创建一个范围,在该范围内,对这些属性的未限定引用将解析为组件,而不是表单:

procedure frmMain.ButtonClick(Sender TComponent);
begin 
   with foo do
   begin
      Caption := 'click me!';
      Tag     := 42;
   end;
end;

另一种方法是不使用 with,而是使用 foo 的属性的限定引用:

procedure frmMain.ButtonClick(Sender TComponent);
begin 
   foo.Caption := 'click me!';
   foo.Tag     := 42;
end;

这显然不是一个“流畅”的API。那么什么是流畅的API?

流畅API

流畅API 是用来描述对象的方法返回对该对象的引用的模式,允许通过链接它们来调用进一步的方法。

继续使用假设的 foo 组件,如果我们想象该组件的开发者提供了一种流畅的API来设置属性,而不是(或者也可以同时)直接将它们公开为属性,那么我们可以想象可以编写类似于这样的代码:

procedure frmMain.ButtonClick(Sender TComponent);
begin 
   foo.SetCaption('click me!')
      .SetTag(42);
end;

SetCaption()SetTag() 返回调用它们的对象,允许进一步调用该对象,以进行链接。

这在表面上看起来类似,尽管较少“啰嗦”,与 with 模式有两个关键区别:

  1. 流畅API 调用是单个语句;with 方法涉及多个语句。

  2. 流畅API 调用是对明确标识的对象进行的;使用 with 进行的调用是隐式标识目标对象的。

with 和流畅API 的混淆可能源于 with 与流畅API 结合使用实际上只是语法的一种变化:

procedure frmMain.ButtonClick(Sender TComponent);
begin 
   with foo do
   begin
     SetCaption('click me!')
     SetTag(42);
   end
end;

但关键的区别仍然存在。上面的代码是使用 foo 隐式执行的两个语句,而在流畅变体中,只有一个语句明确调用了 foo

一个显着的额外差异是,with 可以与多个符号一起使用(如果您绝对决心使自己和同事的生活更加困难的话 :))。这有可能让人难以一眼看出 with 范围中的哪些引用解析为哪些符号。

另一方面,流畅API 总是从某个对象开始,链条总是与该对象继续。永远不会有关于您正在处理哪个符号的疑问或困惑。

仅仅为了好玩,我们可以展示 with 和流畅性是正交的,方法是看如何将这两者一起使用以创建真正的怪物:

procedure frmMain.ButtonClick(Sender TComponent);
begin 
   with foo do
   begin
     SetCaption('click me!')
     .SetTag(42);
   end;
end;

那么它们有何相似之处?

唯一真正的相似之处在于流畅API 提供了不必为每次调用限定名称的好处(或更准确地说,将每个后续调用的限定作为上一次调用的一部分)。

with 的一个大问题是,到今天为止,调试器和IDE工具仍然无法像编译器一样解析范围内的符号,这可能在尝试调试 with 语句中的代码时导致重大问题。在调试器中检查 with 范围的值可能会解析为错误的符号,从而给出错误的值。

流畅API 有其自己的调试挑战,最明显的是,流畅API 调用的链接序列是单个语句,只能放置一个断点。

那么您应该使用哪个?

作为API的使用者...

如果您面临一种情况,您没有一个流畅的API可供使用,那么在 with 和流畅API 之间没有选择。只有一个决定是否使用 with。这是一个长期存在且争议不断的问题。

对于这个问题的简短答案是(依我看):除非/直到您有信心确定安全且有益的情况(非常,非常少)使用 with。与此同时,默认选择“不要”。

如果有一个流畅的API,那么请使用它。它提供了非常相似的语

英文:

There are some (very, very few) use cases where with is "safe" to use but should otherwise be avoided.

So I am specifically not going to get into (in detail) why you should or should not use with and stick to answering the second part of your question: the apparent misunderstanding that with and fluent APIs are somehow directly related when they really are not.

with

with takes one or more symbols and creates a scope in which those symbols takes precedence over other symbols with the same name in the containing scope:

So if we imagine we are writing code on some form and need to set multiple properties of some component on that form where some of those properties are also present (i.e. with the same name) on the form class itself, we can use with to create a scope in which an unqualified reference to those properties resolves to the component, not the form:

procedure frmMain.ButtonClick(Sender TComponent);
begin 
   with foo do
   begin
      Caption := 'click me!';
      Tag     := 42;
   end;
end;

The alternative is to not use with and instead use qualified references to the properties of foo:

procedure frmMain.ButtonClick(Sender TComponent);
begin 
   foo.Caption := 'click me!';
   foo.Tag     := 42;
end;

This is clearly not a "fluent" API. So what is?

Fluent API

A fluent API is a term used to describe a pattern where methods of an object return a reference to that object, allowing further methods to be called by chaining them.

Sticking with the hypothetical foo component, if we imagine that the developer of that component had provided a fluent api for setting properties instead of (or perhaps as well as) directly exposing them as properties, then we could imagine it might be possible to write code similar to this:

procedure frmMain.ButtonClick(Sender TComponent);
begin 
   foo.SetCaption('click me!')
      .SetTag(42);
end;

SetCaption() and SetTag() return the object on which they are called, allowing further calls to that object to be chained together.

This superficially looks similar, although less "wordy", to the with pattern, with two key differences:

  1. the fluent api call is a single statement; the with approach involves multiple statements

  2. the fluent api calls are made to an explicitly identified object; the calls made using with implicitly identify the target object

The confusion of with and fluent APIs perhaps stems from the fact that the use of with in conjunction with a fluent api is essentially just a variation in grammar:

procedure frmMain.ButtonClick(Sender TComponent);
begin 
   with foo do
   begin
     SetCaption('click me!')
     SetTag(42);
   end
end;

But the key differences remain. The above code is two statements implicitly using foo vs the single statement explicitly calling foo in the fluent variant.

A significant additional difference is that with can be used with multiple symbols (if you are absolutely determined to make your life and that of your colleagues difficult :)). This has (hopefully) obvious potential to make it difficult to tell (at a glance) which references in a with scope resolve to which symbols.

A fluent api, on the other hand, always starts with some object, and the chain always continues with that object. There is never any question or doubt about what symbol you are dealing with.

Just for fun, we can show that with and fluency are orthogonal by looking at how the two could be used together to create a real monstrosity:

procedure frmMain.ButtonClick(Sender TComponent);
begin 
   with foo do
   begin
     SetCaption('click me!')
     .SetTag(42);
   end;
end;

So what is the similarity?

The only real similarity is that a fluent api provides much the same benefit of not having to qualify every call (or, more accurately, by making the qualification of each subsequent call part of the previous call).

One of the big problems with with is that the debugger and IDE tooling to this day remains unable to resolve the symbols in the same way that the compiler does, which can lead to significant issues when trying to debug code in a with statement. Inspecting a with scoped value in the debugger can resolve to the wrong symbol, giving the wrong value.

Fluent APIs have their own debugging challenges, the most obvious being that a chained sequence of fluent api calls is a single statement on which only a single breakpoint may be placed.

Which should you use?

As the consumer of an api...

If you are faced with a scenario where you don't have a fluent api to work with, there is no choice between with and a fluent api. Only a decision whether to use with or not. Which is a long-running and heated debate.

The short answer to this is (IMHO): don't use with unless/until you have confidently identified the (very, very few) edge cases where it is safe and beneficial to do so. In the meantime, default to "don't".

If there is a fluent api, then use it. It provides very comparable syntactic shorthand without the pitfalls of with, though at the expense of some lack of granularity when it comes to breakpoints, as mentioned.

The third alternative, of course, is to use neither with nor fluency and simply take a reference to the "root" of the fluent api chain and use that in separate statements rather than chaining calls.

As the developer of an api

Think about how your code will be used and consider whether a fluent api makes sense. "Builder" type APIs very commonly use fluent APIs, and there are other use cases, but some fluent APIs are very unintuitive and can be cumbersome to use; they are not a panacea.

And if you run into an edge case in your api where fluency isn't possible, you risk leaving your consumers in the uncomfortable position of using fluency in some areas but not being able to in others, which can be very frustrating having to remember which parts of your api work in which ways.

I hope that helps.

答案2

得分: 0

以下是翻译好的部分:

"上面的解释很好,但从你的问题中不清楚你要找什么。这里有一个稍微不同的方法。

假设你有一个名为 TFoo 的类,其中有几种不同的方法。在你的代码中,你可能会写类似这样的内容:

foo.turnOn;
foo.doThis;
foo.Caption := 'a caption';
foo.Update( aRecPtr );
foo.turnOff;

使用 'with' 可以非常容易地完成:

with foo do
begin
turnOn;
doThis;
Caption := 'a caption';
Update( aRecPtr );
turnOff;
end;

在这种情况下,使用 'fluent' 方法不会直接起作用,因为这些方法中没有一个可能会返回对 foo 的引用。(你可能注意到,VCL 方法都不是为此设计的。)因此,你需要重写你需要的方法(或整个类),以便你可以这样做:

foo.turnOn
.doThis
.Caption( 'a caption' )
.Update( aRecPtr )
.turnOff;

这并不难,你可以为 'foo' 的任何类型创建一个 Helper 类。每个 Helper 类都需要是返回基本类型的函数:

type
TFooHelper = class helper for TFoo
function turnOn : TFoo;
function Caption( const str : string ) : TFoo;
end;

function TFooHelper.turnOn : TFoo;
begin
self.turnOn; // Self 指的是 TFoo 类型,而不是 TFooHelper
Result := self;
end;

function TFooHelper.Caption( const str : string ) : TFoo;
begin
self.Caption := str;
Result := self;
end;

等等。"

英文:

The above explanation is great, but it's unclear from your question what you're looking for. Here's a slightly different take.

Suppose you have a class TFoo with a few different methods. In your code, you might have an occasion to write something like this:

foo.turnOn;
foo.doThis;
foo.Caption := 'a caption';
foo.Update( aRecPtr );
foo.turnOff;

The use of 'with' can be done very easily:

with foo do
begin
  turnOn;
  doThis;
  Caption := 'a caption';
  Update( aRecPtr );
  turnOff;
end;

Using a 'fluent' approach won't work directly in this case because none of these methods are likely to return a reference to foo. (You might notice that none of the VCL methods are designed for this.) So you'll need to rewrite the ones you need (or the entire class) so you can do the following:

foo.turnOn
   .doThis
   .Caption( 'a caption' )
   .Update( aRecPtr )
   .turnOff;

This isn't very hard, and you can create a Helper class for whatever type 'foo' is. Each one needs to be a function that returns the base type:

type
TFooHelper = class helper for TFoo
  function turnOn : TFoo;
  function Caption( const str : string ) : TFoo;
end;

function TFooHelper.turnOn : TFoo;
begin
  self.turnOn;  // Self refers to TFoo type, not TFooHelper
  Result := self;
end;

function TFooHelper.Caption( const str : string ) : TFoo;
begin
  self.Caption := str;
  Result := self;
end;

and so forth.

huangapple
  • 本文由 发表于 2023年6月27日 21:42:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/76565496.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定