I recently came up to an interesting question, what should fluent methods return? Should they change state of current object or create a brand new one with new state?
In case this short description is not very intuitive here's an (unfortunaltely) lengthy example. It is a calculator. It performs very heavy calculations and that's why he returns results via async callback:
public interface ICalculator {
// because calcualations are too lengthy and run in separate thread
// these methods do not return values directly, but do a callback
// defined in IFluentParams
void Add();
void Mult();
// ... and so on
}
So, here's a fluent interface which sets parameters and callbacks:
public interface IFluentParams {
IFluentParams WithA(int a);
IFluentParams WithB(int b);
IFluentParams WithReturnMethod(Action<int> callback);
ICalculator GetCalculator();
}
I have two interesting options for this interface implementation. I will show both of them and then I'll write what I find good and bad each of them.
So, first is a usual one, which returns this:
public class FluentThisCalc : IFluentParams {
private int? _a;
private int? _b;
private Action<int> _callback;
public IFluentParams WithA(int a) {
_a = a;
return this;
}
public IFluentParams WithB(int b) {
_b = b;
return this;
}
public IFluentParams WithReturnMethod(Action<int> callback) {
_callback = callback;
return this;
}
public ICalculator GetCalculator() {
Validate();
return new Calculator(_a, _b);
}
private void Validate() {
if (!_a.HasValue)
throw new ArgumentException("a");
if (!_b.HasValue)
throw new ArgumentException("bs");
}
}
Second version is more complicated, it returns a new object on each change in state:
public class FluentNewCalc : IFluentParams {
// internal structure with all data
private struct Data {
public int? A;
public int? B;
public Action<int> Callback;
// good - data logic stays with data
public void Validate() {
if (!A.HasValue)
throw new ArgumentException("a");
if (!B.HasValue)
throw new ArgumentException("b");
}
}
private Data _data;
public FluentNewCalc() {
}
// used only internally
private FluentNewCalc(Data data) {
_data = data;
}
public IFluentParams WithA(int a) {
_data.A = a;
return new FluentNewCalc(_data);
}
public IFluentParams WithB(int b) {
_data.B = b;
return new FluentNewCalc(_data);
}
public IFluentParams WithReturnMethod(Action<int> callback) {
_data.Callback = callback;
return new FluentNewCalc(_data);
}
public ICalculator GetCalculator() {
Validate();
return new Calculator(_data.A, _data.B);
}
private void Validate() {
_data.Validate();
}
}
How do they compare:
Pro first (this) version:
easier and shorter
commonly used
seems to be more memory-efficient
what else?
Pro second (new) version:
stores data in separate container, allows to separate data logic and all handling
allows us to easily fix part of data and then fill in other data and handle it separately. Take a look:
var data = new FluentNewCalc() .WithA(1); Parallel.ForEach(new[] {1, 2, 3, 4, 5, 6, 7, 8}, b => { var dt = data .WithB(b) .WithReturnMethod(res => {/* some tricky actions */}); // now, I have another data object for each value of b, // and they have different callbacks. // if I were to do it with first version, I would have to create each // and every data object from scratch var calc = dt.GetCalculator(); calc.Add(); });
What could be even better in second version?
I could implement WithXXX method like this:
public IFluentParams WithXXX(int xxx) { var data = _data; data.XXX = xxx; return new FluentNewCalc(data); }
and make _data readonly (i.e. immutable) which some smart people say is good.
So the question is, which way do you think is better and why? P.S. I used c# but the could well apply to java.