Reputation: 6768
If you can figure out how to rename this question, I'm open for suggestions.
In the Dart language, one can write a class with final fields. These are fields that can only be set before the constructor body runs. That can be on declaration (usually for static constants inside a class), in an initialiser list syntax when declaring the constructor or using the this.field
shorthand:
class NumBox{
final num value;
NumBox(this.value);
}
Let's say I actually needed to do some processing on instance creation and can't just initialise the field before the constructor. I can switch to using a private non-final field with a getter:
class NumBox{
num _value;
NumBox(num v) {
_value = someComplexOperation(v);
}
num get value => _value;
}
Or I can get a similar behavior using a factory constructor:
class NumBox{
final num value;
factory NumBox(num v) {
return new NumBox._internal(someComplexOperation(v));
};
NumBox._internal(this.value);
}
I hit a similar bump when I tried learning Dart a few years back and now that I have more baggage, I still don't know. What's the smarter way to do this?
Upvotes: 1
Views: 3055
Reputation: 71693
You should design your API with your user in mind, then implement it in whatever way is simpler and more maintainable to you. This question is about the latter part.
Making fields final is great when it's possible, and when it isn't, making them private with a public getter is a good alternative. It's your choice what to do, because it's you who is going to maintain your class, nobody else should need to look behind the public API.
If you need a complex computation, Günther Zöchbauer's suggestion is the first to turn to: Use a helper function. In some cases, you can even do it inline
class MyThing {
final x;
MyThing(args) : x = (() { complex code on args; return result;} ());
}
It gets ugly quickly, though, so having it as a static helper function is usually better.
If your complex computation doesn't match this, which ususally means that there is more than one field being initialized with related values, then you need a place to store an intermediate value and use it more than once when initializing the object.
A factory constructor is the easy approach, you can compute everything you need and then call the private generative constructore at the end. The only "problem" is that by not exposing a generative constructor, you prevent other people from extending your class. I quoted "problem" because that's not necessarily a bad thing - allowing people to extend the class is a contract which puts restrictions on what you can do with the class. I tend to favor public factory constructors with private generative constructors even when it's not needed for any practical reason, just to disable class extension.
class MyClass {
const factory MyClass(args) = MyClass._; // Can be const, if you want it.
const MyClass._(args) : ... init .. args ...;
}
If you need a generative constructor, you can use a forwarding generative constructor to introduce an intermediate variable with the computed value:
class MyClass {
final This x;
final That y;
MyClass(args) : this._(args, _complexComputation(args));
MyClass._(args, extra) : x = extra.theThis, y = extra.theThat, ...;
}
All in all, there is no strict rule. If you prefer final fields, you can do extra work to make them final, or you can just hide the mutability behind a getter - it's an implementation and maintainability choice, and you're the one maintaining the code. As long as you keep the abstraction clean, and keeps track of what you have promised users (generative constructor? const constructor?) so you won't break that, you can change the implementation at any time.
Upvotes: 4
Reputation: 657416
A factory constructor is a good way, it allows to pre-calculate without limitations any values that you then pass to a normal constructor to forward to final fields.
An alternative way is initializer list which is executed before the constructor body and therefore allows to initializer final fields:
class NumBox{
final num value;
NumBox(num v) : value = someComplexOperation(v)
}
In the initializer list you are not allowed to read this
because the instance isn't fully initialized yet.
Upvotes: 5