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.
When I am trying to answer such a question in my application design I always think about what a person using my code in his application would expect.
Take, for instace, the C# DateTime
type. It is a struct and therefore immutable. When you ask for
var today = DateTime.Now;
var tomorrow = today.AddDays(1);
what would you expect if you didn't know that DateTime
is immutable? I would not expect that today is suddenly tomorrow, that would be chaos.
As for your example, I would imagine that numbers are being processed using only one instance of the calculator, unless I decide otherwise. It makes sense, right? When I am writing an equation, I don't write each expression on a new line. I write it all together along with a result and then I jump to the next line in order to separate concerns.
So
var calc = new Calculator(1);
calc.Add(1);
calc.PrintCurrentValue(); // imaginary method for printing of a current value of equation
makes perfect sense to me.