22

In an object oriented language like Java or PHP (other perspectives welcome as well) if I use a fluent interface like this:

my_pizza = (new Pizza).withTopping("pineapple");
another_pizza = my_pizza.withGarlic(true);

would one expect withGarlic() to clone the pizza or would one expect my_pizza to change (into a culinary abomination)?

Christophe
  • 74,672
  • 10
  • 115
  • 187
AndreKR
  • 577
  • 3
  • 11
  • 3
    The fluent interface itself doesn't imply cloning (could be either way), and as for "with", I guess it depends. If the data structure were immutable, then the naming convention makes more sense and it would also imply a copy. But I feel like "with" suggests less an object that mutates in place, and more some kind of an incremental factory pattern (Joshua Bloch’s Builder), that will return the completed object at the end. In any case, the implications of using "with" in this way could be specific to a team (an internal convention), or vary by circumstance. – Filip Milovanović Aug 08 '21 at 12:59
  • In C#9 new keyword `with` will do shallow copy of the object, but there are no such convention that fluent interface should clone the object or should update existing one. – Fabio Aug 08 '21 at 20:13
  • 4
    A mutating method should have a transitive verb in it (e.g., `addTopping()`), but fluent interfaces break these kinds of rules all the time to make the code look more englishy, so really you just can't tell. When *I* write a "with method", it clones. – Matt Timmermans Aug 09 '21 at 12:33
  • Why in the world does this interesting question about API design have 2 close votes? Sometimes the community baffles me... – Heinzi Aug 10 '21 at 16:42

11 Answers11

37

Looking at this code I’d have no idea. Semantically you did say it’s another pizza. But since this is of type Pizza and not a PizzaBuilder that gives you a pizza object only after you call the build method, it’s anyone’s guess how your fluent interface works.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
  • 16
    Which is why **this should be documented**!  A doc comment (or equivalent in the language in question) should mention anything not immediately obvious from the method's name/type/params — and this is exactly the sort of thing that the caller needs to know. – gidds Aug 08 '21 at 16:49
  • 9
    As an aside, this sort of ambiguity is one of the hazards of how fluent interfaces have to compromise the normal meaning of the code.  One of the things I like about Kotlin is that its scoping functions let you express this sort of thing concisely without needing a fluent interface at all. – gidds Aug 08 '21 at 16:51
  • Honestly, even with a `PizzaBuilder` it's not clear whether the builder would be modified or a new builder created. In a static language this may be represented at the type level, but apart from that... – Matthieu M. Aug 08 '21 at 17:04
  • @MatthieuM. I can’t think of any mainstream AOT-compiled language with first-class support for “incremental types” (if that’s what they’re called), it’s very close to Hindley–Milner though… But having them wound be great. – Dai Aug 09 '21 at 10:26
  • 4
    @Dai: Are you thinking about Session Types? If so Rust can model them fairly easily, and I expect C++ too. I wasn't describing those though, simply that in Java, for example, you can't tell from `PizzaBuilder withGarlic(boolean w)` whether it modifies `this` (and returns `this`) or whether it clones `this` and returns a deep copy with the garlic attribute modified. On the other hand, in C++ or Rust, it's visible at the signature level (const qualifier/absence of mut qualifier). – Matthieu M. Aug 09 '21 at 10:52
  • @MatthieuM. You're assuming that they write C++ with const correctness there. I find that to be a minority of the code I've dealt with. I wouldn't assume the absence of a const to mean that the function isn't const, just that it's presence means it is. – Gabe Sechan Aug 09 '21 at 20:55
  • 1
    @GabeSechan: I'm sorry to hear that you worked in such environments; fortunately my experience is quite different, with mostly const-correct code. The point, though, is that at least the language offers facilities for it... bad developers can develop bad code in any language, it's a poor standard to judge things by. – Matthieu M. Aug 10 '21 at 07:04
  • @MatthieuM. Don't forget `const`-incorrect code that abuses `const_cast` :) – Dai Aug 10 '21 at 09:13
  • @MatthieuM. Actually for many years at the beginning of my career ignoring const correctness was the standard. It doesn't give all that much, and interacting with non const correct code from const correct code was always problematic, to the point almost every place I've ever worked for and did C++ decided const wasn't worth it. Maybe that's changed, but I kind of doubt it. I think you've worked at some unusual places. – Gabe Sechan Aug 10 '21 at 13:38
27

No, the naming prefix with does not tell if it's cloning or mutating. There are popular examples of fluent interfaces using a mutating with and some language-specific conventions (e.g. Java) that recommend with for cloning.

Moreover, An interface designed with method chaining can use either a functional approach (returning a new object each time, i.e. copy on write, no side effects) or a self-referencing approach (returning the potentially modified original object).

Fluent interface are often based on the self-referencing approach in a non-functional context. The goal is often compactness, by allowing several operations on the same object in a single expression instead of separate statements:

kitchen.order(pizzaFactory.getPizza("crusty").withTopping("x").withGarlic());

It is confusing to mix both styles and to rely on a naming convention that is not necessarily known by the larger OOP community. So whatever you chose, document it and be consistent across all the related classes.

user
  • 103
  • 4
Christophe
  • 74,672
  • 10
  • 115
  • 187
  • 1
    Just to add on- the reason that many builder implementations use self referential is that creating a new object is generally somewhat expensive. Changing a field is cheap. Some builder patterns (StringBuilder in Java for example) exist explicitly because a self referential model is so much cheaper than the functional model (string concatenation is expensive because strings are constant and each concatenation needs to create a new one). Which is why outside of functional languages, self referential is the norm for builders (although not exclusively so). – Gabe Sechan Aug 09 '21 at 20:59
12

Update based on the comments below: Just to clarify, this answer is not a judgement of whether the fluent pattern is generally good or bad, it's about a relative trade-off in the specific situation where it is used to modify a single mutable object.


I'd suggest re-evaluating the idea of using a fluent interface in the first place if the only goal is simply to send a series of messages to the same object.

Before fluent interfaces were popular, many programs using OO languages would be full of simple 'message passing' code, which while slightly verbose, made its intent absolutely 100% clear in that these methods are all modifying the same object:

ButtonWidget widget = new ButtonWidget();
widget.dimensions(100, 60);
widget.backgroundColour(Colour.BLUE);
widget.text("Click Me!");

Is there anything wrong with that aside from the fact that the variable widget appears on multiple lines? There's no ambiguity about other objects being created in memory, and having the variable there multiple times is not hurting readability at all - in fact, it's adding useful context in making it 100% clear that all these messages are indeed passed to the same object.

Consider code which is identical in its behaviour, but using a fluent style:

ButtonWidget widget = new ButtonWidget()
    .dimensions(100, 60)
    .backgroundColour(Colour.BLUE)
    .text("Click Me!");

Maybe that took fewer keystrokes to write, and saved a couple of seconds of typing, but the cost is it not being immediately obvious to the reader whether there's a single ButtonWidget or whether some or all of those methods returned different objects.

If there is only one object, then what has the fluent style gained over traditional OO message-passing style? And why not just go back to that traditional style which everybody already knows and recognises?

A key benefit of fluent interfaces is the way in which it promotes immutability; if you're not going to take advantage of that, then why even bother in the first place?

Ben Cottrell
  • 11,656
  • 4
  • 30
  • 41
  • 1
    I didn't really ask whether the fluent interface is a good pattern in the first place, but I appreciate the discussion. The huge drawback of the non-fluent solution is that if `widget` is a one-off object that you immediately pass as a parameter (`layout.addWidget((new ButtonWidget).dimensions(100, 60))`), the name `widget` is now "burned" and if you want to add another widget of a different type (`SliderWidget`) you have to come up with a new name. `widget2`? – AndreKR Aug 08 '21 at 18:05
  • The drawback is that you cannot simply hand the result into another method. That helps with brevity but also with clarity. I.e. if you do a chaining of "with"s followed by a final application of an operation either on the object itself or the the application of the withs is wrapped in a method application it's clear on first sight that the modified object is operated on. e.g. `ptrint(user.withName('tony').withId(45))`. In general it works much nicer if you follow a somewhat functional style. Obviously it depends as so much on what your context. is. – Frank Hopkins Aug 08 '21 at 18:06
  • 1
    @AndreKR I pass absolutely no judgement about whether the fluent pattern is good or bad - personally I like it a lot in many of the C# libraries I've seen it used with (where I believe objects are immutable); however that has nothing to do with this answer. The point I'm making is when the only benefit of the fluent pattern is a slight reduction in then you need to factor in how that might also affect clarity. It is ultimately for you to decide whether that's a trade-off you're happy with (ideally put it to a peer review with others who might be working in that same project/code). – Ben Cottrell Aug 08 '21 at 18:38
  • 1
    @FrankHopkins I don't think brevity has such a strong correlation with clarity as you suggest; brevity can often hinder clarity (i.e. how easy and obvious it is for programmers to 'see' intent and what code is doing without needing to think too hard). I see and use a lot of fluent interfaces in C# (LINQ, Entity Framework, and others) and find most issues are with individual lines/statements doing far too many things. I find it rare for programmers to have worsened clarity with "too many" statements except where those statements are so trivial/banal that separate statements are pointless. – Ben Cottrell Aug 08 '21 at 19:08
  • @BenCottrell my argument isn't about brevity, it's about clarity. if you see a function application on the object you modify in the same expression it is clear which object with which settings the method is operating on. If you separate it by having an explicit object, setting attributes and then further down the line operate on it it's less clear. It's not a huge thing but it's a thing. Especially if you have multiple objects in the same scope. Now are there also other ways to make it clear (e.g. by moving the setting stuff into a separate method), sure, but they have their own overhead. – Frank Hopkins Aug 08 '21 at 19:23
  • @BenCottrell this can also make for much nicer reading as the intent for which the parameters are set is directly clear e.g. "lockup(suspect.withHandcuffs(true).withJacket(prisonJacket))" is clearer than "suspect.setHandcuffs(true); suspect.setJacket(prisonJacket); lockup(suspect)" Because now I don't wonder why the attributes are changed, I directly see that I'm about to put the suspect away for good and therefore adjust the attributes accordingly. Does this apply always? No. Are there convoluted one-liners, sure. But a lot can be resolved by proper result variable naming. – Frank Hopkins Aug 08 '21 at 19:26
6

I think the other answers clearly show that this is ambiguous, and when you stumble upon such code and the difference matters, you should check the implementation (or documentation, though I would personally go for the implementation first; after all, it is the ground truth, and often easier to find than the relevant docs).

As for my first expectation, I'd assume that withX would mean cloning. For mutating method, I'd rather expect a name with verb in it, like setX or addX. That is what I'd use when building a fluent interface, unless there is already a different convention in the codebase or in used libraries. It is more important to stay consistent than to use some "best naming" for a single class - the principle of least surprise is paramount.

Frax
  • 1,844
  • 12
  • 16
4

Technically, the answer is "no"

Your question is

In a fluent interface with “with”, is cloning expected?

And the "technically correct" answer is that, no, "with" is not a "keyword" that implies either way.

One of two hard things

There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton

"Naming things" is where the problem lies. If we look at a function name and don't know if that means one thing or the other, the solution is not to philosophize what it really means. The solution is to change the method name.

I don't think anybody would expect an instance method called .addTopping() to clone the instance.
On the other side, a method .cloneWithTopping() also doesn't leave a lot of room for doubt.

Cloning is a special case

Cloning is only useful for cases where you want to re-use a set of configuration options - e.g. a base_pizza with tomato sauce and cheese.

In those special cases, it's also worth it to explicitly mention that, e.g. with an own .clone() call. After all, that kind of composition is what fluent interfaces are for, eh?

Suggestion: Use a builder

Fluent interfaces are for situations where you need to repeatedly "compose" something with many options in a "hardcoded" way. If you have that many options, all the logic for setting those options will make up quite a bit of code (the obvious assumption being that "pizza" is a simplified example).

All of that code belongs to the same responsibility "creating a pizza", so it makes sense to put it into an own PizzaBuilder class - allowing the code in Pizza to be focused on what happens with a (completed) Pizza.

The builder pattern is well-established and doesn't imply cloning, let alone at every step (but it wouldn't hurt to use the more clear naming here, either).

base_pizza_builder = (new PizzaBuilder)
                   .withTomatoSauce()
                   .withCheese("mozarella");

pizza_margherita = base_pizza_builder.build();

pizza_hawaii_builder = base_pizza_builder
                   .clone()
                   .withTopping("pineapple")
                   .withTopping("ham");

pizza_hawaii = pizza_hawaii_builder.build();
R. Schmitz
  • 2,608
  • 1
  • 16
  • 28
  • 3
    I don't see how implementing a separate `PizzaBuilder` solves this problem - it just moves it to a different place. Sure, your `pizza_margerita` won't have pineapple on it since it's a separate object and you called `pizza_builder.build()` before putting pineapple and ham on it, but what if you use the same `pizza_builder` instance to create a `pizza_salami = pizza_builder.withTopping("salami").build()` - will it share its toppings with the hawaii builder or not? – Bergi Aug 08 '21 at 13:45
  • 1
    @Bergi Isn't that pretty straightforward? The problem is that `withTopping()` isn't clear about if it return the modified original or a copy. That's not even a question if `withTopping()` returns a `PizzaBuilder` . The builder pattern is pretty established as having a `build()` method that creates a new instance with the previously given settings. – R. Schmitz Aug 08 '21 at 16:46
  • @Bergi Regarding your second question: What are you envisioning there? Show me somebody who thinks that a method named `with[x]()` should _remove_ other things and I'll show you a bad programmer. – R. Schmitz Aug 08 '21 at 16:52
  • It's still not clear if the `pizza_hawaii` has tomato sauce and mozzarella, is it? – AndreKR Aug 08 '21 at 18:02
  • builder patterns are fine, but as others have said this just shifts the discussion to the Builder class - does every with of the builder clone the builder? Because the presence of the build method that creates *another type* of object doesn't imply that it does or doesnt. That aside, a pizza might not build itself, but you might have a pizza and want to add cheese to it because you forgot. Are you building a new pizza? Or not just amending the existing one? So if you go by analogy, modifications after initial building belong to the Pizza (at least it needs to provide a way to be modified). – Frank Hopkins Aug 08 '21 at 18:11
  • @AndreKR It's created by calling `build()` on a builder that has `withTomatoSauce()` and `.withCheese("mozarella")` called on it beforehand, so why would those not be on there? – R. Schmitz Aug 08 '21 at 18:21
  • @R.Schmitz True. `build()` *might* reset the builder, but that would be a bit of a stretch. – AndreKR Aug 08 '21 at 18:47
  • @R.Schmitz I think your latest edit broke the code, `pizza_builder = (new PizzaBuilder).….build()` builds a pizza not a builder. – Bergi Aug 08 '21 at 20:54
  • @R.Schmitz The question is in `a = pizza_builder.withTopping("ham"); b = pizza_builder.withTopping("salami")` whether `a == pizza_builder` and `pizza_builder == b`, i.e. does `pizza_builder.build()` create a pizza with both toppings? The desired behavior would be that `a.build()` builds ham pizzas and `b.build()` builds salami pizzas. – Bergi Aug 08 '21 at 20:57
  • @AndreKR I reworked the answer to explain the steps in between how I got to suggesting a builder pattern. – R. Schmitz Aug 08 '21 at 21:15
  • @Bergi I edited the code example and I think there's no room left for that confusion anymore now. – R. Schmitz Aug 09 '21 at 17:03
1

I would say that many readers would expect that if they perform:

thingWithX = someThing.withX(123);
thingWithY = someThing.withY(456);
anotherWithX = someThing.withX(123);

the first call to withX would not affect the object referred to by thingWithY, and the call to withY would not affect the one referred to by anotherWithX. The easiest way to achieve such semantics would often be to have withX and withY return "near clones" of the object in question, but if someThing was immutable I don't think that readers would (or should) necessarily expect that thingWithX and anotherWithX couldn't compare equal to each other. Among other things, readers would/should not be astonished by an implementation where, if e.g. property X of the original object happened to be equal to 123, a withX(123) call simply returned the object upon which it was invoked).

Certainly there exist interfaces which use the preposition "with" in methods that alter the object upon which they are invoked, but I would suggest that people designing interfaces should avoid using that preposition in such cases except, perhaps, on objects which are explicitly intended for temporary usage only. While .NET doesn't provide a practical means for a class C to export a type T in such a way that client code can call methods of C that return objects of type T, and pass those objects to other methods of C, but not otherwise store references to such objects, one could specify that certain methods' return types are meant to be used exclusively as a chaining point for additional method calls, and perhaps give the types names that would attempt to make that point clear to anyone who sees them.

supercat
  • 8,335
  • 22
  • 28
1

This will depend on the language and the conventions of your project.

In , we might use immutable object state and rvalue references to make it a logical copy but in actually not.

Pizza my_pizza = Pizza{}.withTopping("pineapple");
Pizza another_pizza = my_pizza.withGarlic(true).withTopping("anchovies");

the first .with would detect it was being called on an rvalue (a temporary or value about to be recycled) and modify the internal state; the second .with would know it is on an lvalue, and create an actually different object. The third would again detect it is on a temporary (rvalue) and modify itself.

With well written immutable state, even making a slightly modified duplicate object should be relatively cheap (on the order of dozens of instructions if done right).

Languages without these features (detecting temporary objects and recycling their state, and efficient immutable state tricks) are going to have to decide between "costly making useless clones" and "mutating object state in unexpected ways", or change the interface (such as a builder, or have the caller state if they are cloning or not).

In C++23, the signatures would look like:

struct Pizza {
  Pizza withTopping(this auto&&, std::string_view);
  Pizza withGarlic(this auto&&, bool);
};
Yakk
  • 2,121
  • 11
  • 10
  • Note that builder still has exactly the same interface. It just reduces the scope of possible confusion by not doing anything useful before converting to the final object, hopefully removing the desire to pass it around and losing track of the references. – Jan Hudec Aug 11 '21 at 05:06
1

would one expect withGarlic() to clone the pizza or would one expect my_pizza to change (into a culinary abomination)?

I am afraid, no one should expect anything that is not clearly specified in an API's docs or requirements.

For example, a with method on an immutable object will clone an object.

On the other hand, a with method on a mutable object might either mutate the object or return a clone with the with mutation applied to it. The nature of the object with the fluent interface will determine what to expect.

In fact, two classes, A and B might implement the same fluent interface and one implement with by returning itself after a state change/transition, and the other have a with method that returns a clone.

Unless the interface docs specifies the semantics of a with method, this would be perfectly valid (though not ideal, and most certainly confusing.)

All of these possibilities are good or bad design choices depending on the context. Meaning, what are the requirements?

Therefore, we cannot expect anything from a fluent interface (or the object implementing it) unless we know the nature of the object itself.

Is the object implementing this hypothetical fluent interface of an immutable type?

Or is the object mutable? Or is the object mutable as well as a factory or composer (where the with method returns a clone rather than itself)?

Does the interface itself specifically documents a requirement on mutability or immutability (and a fluent interface by itself does not require either unless explicitly expressed)?

So, it all boils down to the question, "what kind of object this is?"

luis.espinal
  • 2,560
  • 1
  • 20
  • 17
1

It depends on the conventions of the language.

In a typical statically typed object oriented language -- C++, Java, C#, etc. -- if I saw that code I would assume that the withX methods are mutating the self or this object. Afterall, in C++ if (new Pizza).withTopping("pineapple") was not returning the newly allocated pizza object it would be a memory leak. In C# or Java I would assume that the designer of the fluent interface would know to not be so careless with the number of temporary objects that will end up getting garbage collected to implement such a design with copies.

In a functional language I would expect the return values to be copies, if a mutating version was even possible.

That said, however, I would view such methods as being non-standardly named in an OOP context by not starting with a verb. Prepositions are not verbs. Prepositions like that would look less out of place in a functional context where mutating functions are rarer.

jwezorek
  • 179
  • 3
  • In C++ `(new Pizza).withTopping("pineapple")` won't compile because dumb pointers don't have methods. – Jan Hudec Aug 11 '21 at 04:53
  • In C++ there are also three different methods to declare the method. `Pizza *withTopping(…)`, which is the archaic, deprecated way, `Pizza &withTopping(…)`, which obviously does not clone (because references don't transfer ownership), and `Pizza withTopping(…) &&`, which obviously clones (it returns by value) and hopefully the compiler can optimize away the move. The last one is the sane thing to do in modern C++. – Jan Hudec Aug 11 '21 at 04:57
0

You need to make clear what your fluent interfaces do or don’t do, and use this consistently, if at all through your whole application. That way I can both read and write code using any fluent interface.

When applied to an instance, a method like “withGarlic” is quite similar to a setter “setGarlic”. I’d assume that withGarlic takes either the original object or a clone, applies the setter, and returns a pointer to the object. (Your objects might be immutable so the setter is private, and withGarlic must clone the object).

Would you want to use a fluent interface except when creating an object? If not then cloning is pointless and inefficient. Do you want to use it outside object creation? But why not use the setter? The fluent interface only makes sense after object creation for immutable objects.

gnasher729
  • 42,090
  • 4
  • 59
  • 119
0

This is to the designer's discretion. There are cases where reusing the same object makes sense. There are cases where creating a duplicate and then applying the requested change to the duplicate makes more sense.

Generally speaking, the only real expectation for this type of builder pattern is that the returned object is the correct one. The only difference that not-cloning would add is that any prior variables referencing the initial object also contain this (future) change, but that is not commonly expected in builder syntax.

As there is no specific expectation that you must do one or the other, do the one that makes the most sense for your internal logic. It'll be easier to avoid side effects by doing cloning, but it might lead to memory cleanup issues (I am bot up to speed on how effective Java's garbage compiler is relative to C#'s).

And, of course, document your design choice in either case.

Flater
  • 44,596
  • 8
  • 88
  • 122
  • *"The only difference that not-cloning would add is that any prior **variables referencing the initial object also contain this (future) change**, but that is not commonly expected in builder syntax."* --- this is a subtly that many people miss. – Greg Burghardt Aug 09 '21 at 11:56
  • 1
    @GregBurghardt: I think it is also the main (if not only) argument for non-cloning. If you don't need that specific behavior you bolded, I see little reason to advocate in favor of the non-cloning approach. But maybe I've just not come across other valid reasons to favor the non-cloning approach. – Flater Aug 09 '21 at 12:04