隆重推出 MODDDELS:一个强大的代码生成器,可让您创建具有编译安全状态、无缝故障处理和轻松单元测试的自验证模型。
动机
假设您想模拟一个Student对象。Student有一个名字、年龄和电子邮件。您可以这样创建您的类/数据类:
class Student {
Student({
required this.name,
required this.age,
required this.email,
});
final String name;
final int age;
final String email;
}
然后,您需要在应用程序的各个部分验证name、age和email。Student模型将在不同的地方使用:例如,一个显示学生资料的小部件,或者一个函数addStudent(Student newStudent),等等……
这种方法存在几个问题:
- 您应该在哪里验证
name、age和email? - 验证完成后,您如何区分一个有效的
Student实例和一个无效的Student实例? - 您如何确保将有效的
Student实例传递给需要它们的函数或小部件,同样,将无效实例传递给专门处理它们的函数或小部件? - 如何在应用程序的各个部分根据哪个字段无效以及为什么无效来区别处理无效的
Student? - 例如,如果您有一个显示
Student的email的小部件。您如何防止任何随机字符串传递给该小部件?
所有这些问题(以及更多)都可以通过使用此包来解决。
Modddels包提供了一种以类型安全和编译安全的方式来验证您的模型并处理其不同状态(有效、无效……)的方法。
- ? 自我验证:您的模型在创建时就会被验证。这样,您将永远不必处理未经验证的模型。
- ? 密封类:您的模型是一个密封类(与 Dart 的先前版本兼容),它为它可以处于的不同状态(有效、无效……)提供联合情况。例如,
Student将是一个具有ValidStudent和InvalidStudent联合情况的密封类。 - ? 故障处理:当您的模型无效时,它会包含负责的故障,您可以随时随地访问它们。
- ? 值相等性和不变性:所有模型都是不可变的,并为数据相等性重写了
operator ==和hashCode。 - ? 单元测试:轻松测试您的模型和验证逻辑。
注意:此包不是一个数据类生成器。它旨在创建应用程序核心的模型(如果您使用 DDD 或 Clean Architecture,这些模型将位于“domain”层)。因此,您应该为 JSON 序列化等事物创建单独的类,手动创建或使用像 freezed 和 json_serializable 这样的工具(这些类通常称为“DataTransferObjects”、“DTOs”或简单地称为“models”)。
文档
请访问 modddels.dev 获取全面的文档、示例、VS Code 代码片段等。
示例
以下是您可以使用 modddels 进行操作的预览。您可以在 example 文件夹中找到完整的示例。
// In this example, [Username], [Age] and [User] are all "modddels".
void main() {
final username =
Username('dash_the_bird', availabilityService: MyAvailabilityService());
final age = Age(20);
final user = User.appUser(username: username, age: age);
// Map over the different validation states of the user.
user.map(
valid: (valid) => greetUser(valid),
invalidMid: (invalidMid) => redirectToProfileInfoScreen(invalidMid),
);
}
// This method can only accept a [ValidUser], i.e a valid instance of [User].
void greetUser(ValidUser user) {
final username = user.username.value;
// Map over the different types of users ([AppUser] or [Moderator])
final greeting = user.mapUser(
appUser: (validAppUser) => 'Hey $username ! Enjoy our app.',
moderator: (validModerator) =>
'Hello $username ! Thanks for being a great moderator.');
print(greeting);
}
// This method can only accept an [InvalidUserMid], i.e an invalid instance of
// [User] specifically because of a failure in the validationStep named "mid".
void redirectToProfileInfoScreen(InvalidUserMid user) {
print('Redirecting to profile ...');
// Checking if the `age` is invalid, and handling the only possible failure.
user.age.mapOrNull(
invalidValue: (invalidAgeValue) => invalidAgeValue.legalFailure.map(
minor: (_) => print('You should be 18 to use our app.'),
),
);
// Checking if the `username` is invalid, and handling its possible failures.
user.username.map(
valid: (validUsername) {},
invalidValue1: (invalidUsernameValue1) {
// Handling failures of the "length" validation.
if (invalidUsernameValue1.hasLengthFailure) {
final errorMessage = invalidUsernameValue1.lengthFailure!.map(
empty: (value) => 'Username can\'t be empty.',
tooShort: (value) =>
'Username must be at least ${value.minLength} characters long.',
tooLong: (value) =>
'Username must be less than ${value.maxLength} characters long.',
);
print(errorMessage);
}
// Handling failures of the "characters" validation.
if (invalidUsernameValue1.hasCharactersFailure) {
final errorMessage = invalidUsernameValue1.charactersFailure!.map(
hasWhiteSpace: (hasWhiteSpace) =>
'Username can\'t contain any whitespace character.',
hasSpecialCharacters: (hasSpecialCharacters) =>
'Username can only contain letters, numbers and dashes.',
);
print(errorMessage);
}
},
invalidValue2: (invalidUsernameValue2) {
// Handling failures of the "availability" validation. This validation is
// part of a separate validationStep than the two previous ones for
// optimization purposes.
final errorMessage = invalidUsernameValue2.availabilityFailure.map(
unavailable: (unavailable) => 'Username is already taken.',
);
print(errorMessage);
},
);
}
贡献
如果您有兴趣贡献,请随时在 GitHub 存储库上通过提交拉取请求、报告错误或创建 issue 来建议新功能。
对于那些想深入研究源代码的人,您可以参考 internal.md 和 architecture.md 文件,以更好地理解包的内部工作原理。
