Search code examples
delphiwith-statementfluent-interface

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


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.


Solution

  • 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.