modddels
modddels copied to clipboard
Add support for async validations
Right now, all validation methods are synchronous, and there is no way to make them async.
@Modddel(
// ...
)
class Age extends SingleValueObject<InvalidAge, ValidAge> with _$Age {
Age._();
factory Age(int value) {
return _$Age._create(
value: value,
);
}
// ⚠️ This can't be async
@override
Option<AgeLegalFailure> validateLegal(age) {
if (age.value < 18) {
return some(const AgeLegalFailure.minor());
}
return none();
}
}
Given that factory constructors can't be async, I only see three options :
Option 1 : Use the builder pattern
We generate a "Builder" that you should instantiate and then call an async method (for example runValidations
) to get your modddel instance.
final builder = AgeBuilder(20);
final age = await builder.runValidations();
-
Advantages :
- Ability to lazily validate a modddel (Although this can be easily accomplished by the user using a package like lazy_evaluation or any other way).
-
Disadvantages :
- Boilerplate code +++
- Different syntax for instantiating a modddel with sync validations versus async validations
Option 2 : Initialize the modddel manually
We add an init
method to the modddel that the user should call, and its returned value is the initialized modddel.
final age = await Age(20).init();
-
Advantages :
- Less boilerplate code
-
Disadvantages :
- ⚠️ RUNTIME ERRORS : If the user forgets to call
init
or accidentally uses the instance created with the factory constructor (Age(20)
) , there will be runtime errors that may be hard to debug - Again : different syntax for instantiating a sync modddel versus async
- ⚠️ RUNTIME ERRORS : If the user forgets to call
Option 3 : Ditch the factory constructors for async methods
We replace the factory constructors with static methods :
// Instead of a factory constructor :
factory Age(int value) {
return _$Age._create(
value: value,
);
}
// We use a static method
static Age create(int value) {
// To accompany this change, this static method is no longer private
// (it never needed to be private since the mixin `_$Age` is private)
return _$Age.create(
value: value,
);
}
The name of the method should be create
for solo modddels, and create{UnionCaseName}
for unions (ex for a Weather
union : createSunny
, createRainy
...).
Then, these static methods can easily be made async :
// Change the return type to a future, and optionally add the async keyword if needed
static Future<Age> create(int value) {
return _$Age.create(
value: value,
);
}
And then for making an instance of the modddel :
// If sync :
final age = Age.create(20);
// If async :
final age = await Age.create(20);
-
Advantages :
- Same syntax for instantiating sync and async modddels (you only add await)
- Solves dart-lang/sdk#9 , because static methods have a return type
- Devs can freely use factory constructors for other purposes
- The forwarding method (
_$Age.create
) has the same name as the static method
-
Disadvantages :
- BREAKING CHANGE
- Static methods can't be unnamed like factory constructors (
Age.create(20)
vsAge(20)
). - Static methods don't preserve generic type information (so if you have generics, you need to forward them in a verbose way)
- Usually, you create an instance with a constructor, and not a static method, so this might be a little less elegant