Were it not for type inference, one could "get the best of both worlds" by implementing not just an immutable FluentThing
class defined in the API, but another, mutable, FluentThingInternalUseOnly
which supported widening conversion to FluentThing
. The Fluent members on FluentThing
would construct a new instance of FluentThingInternalUseOnly
and have that latter type as their return type; the members of FluentThingInternalUseOnly
would operate on, and return, this
.
This, if one said FluentThing newThing = oldFluentThing.WithThis(4).WithThat(3).WithOther(57);
, the WithThis
method would construct a new FluentThingInternalUseOnly
. That same instance would be modified and returned by WithThat
and WithOther
; the data from it it would then be copied to a new FluentThing
whose reference would be stored in newThing
.
The major problem with this approach is that if someone says dim newThing = oldFluentThing.WithThis(3);
, then newThing
wouldn't hold a reference to an immutable FluentThing
, but a mutable FluentThingInternalUseOnly
, and that thing would have no way of knowing that a reference to it had been persisted.
Conceptually, what's needed is a way of having FluentThingInternalUseOnly
be sufficiently public that it can be used as a return type from a public function, but not so public as to permit outside code from declaring variables of its type. Unfortunately I don't know any way to do this, though perhaps some trick involving Obsolete()
tags might be possible.
Otherwise, if the objects being acted upon are complicated but the operations are simple, the best one may be able to do is have the fluent interface methods return an object which holds a reference to the object upon which it was called along with information about what should be done to that object [chaining fluent methods would effectively build a linked list] and a lazily-evaluated reference to an object upon which all appropriate changes had been applied. If one called newThing = myThing.WithBar(3).WithBoz(9).WithBam(42)
, a new wrapper object would be created at each step of the way, and the first attempt to use newThing
as a thing would have to construct a Thing
instance with three changes applied to it, but the original myThing
would be untouched, and it would only be necessary to make one new instance of Thing
rather than three.