Modelos
Clases
Nombrado de modelos
Los modelos deben incluir el sufijo Model
en su nombre y deben utilizar el mismo nombre base de la entidad correspondiente.
class UserModel { }
class ProductModel { }
A. Contextualización de nombres de clases
Los modelos pueden incluir en su nombre el tipo de objeto que representan. Por ejemplo, Dto
para objetos de transferencia de datos, Response
para objetos de respuesta, Request
para objetos de solicitud, etc; respetando la regla de Nombrado de modelos. Se recomienda mantener un estándar en la contextualización del nombrado de modelos dentro del mismo proyecto o paquete.
class UserDtoModel extends User { }
class CreateUserRequestModel extends CreateUserRequest { }
class UpdateUserResponseModel { }
Extensión de la clase entidad
Las clases de modelos deben extender de la clase entidad correspondiente.
class UserModel extends User { }
class ProductModel extends Product { }
A. Excepción de extensión
Esta regla no aplica para modelos que no tengan una clase entidad correspondiente. Por ejemplo, modelos de datos que no representan entidades o modelos de uso limitado a la capa de datos.
Constructores
Constructor generativo
Los modelos deben definir un constructor generativo que acepte todos los parámetros super.
class UserModel extends User {
UserModel({
required super.id,
required super.name,
});
}
A. Uso de delegados para asignación de parámetros super
Los modelos pueden aceptar parámetros de tipos diferentes a los de la entidad correspondiente. En estos casos, el cuerpo del constructor generativo debe realizar la conversión del parámetro delegado en la asignación del parámetro super correspondiente.
class UserModel extends User {
UserModel({
required int id, // 👈 Parámetro `id` delegado
required super.name,
}) : (super.id = id.toString());
// 👆 El constructor de UserModel recibe un int como parámetro id, mientras que el constructor de User espera un String. La conversión se realiza en el cuerpo del constructor del modelo.
}
/// Modelo de enum correspondiente a la entidad UserStatus
enum UserStatusModel {
active('active'),
inactive('inactive');
const UserStatusModel(this.value);
final String value;
UserStatus toEntity() {
switch (this) {
case UserStatusModel.active:
return UserStatus.active;
case UserStatusModel.inactive:
return UserStatus.inactive;
}
}
}
class UserModel extends User {
UserModel({
required super.id,
required super.name,
required UserStatusModel status,
}) : super(
status: status.toEntity(), // 👈 Conversión del enum model al enum entidad
);
}
Constructor factory fromMap
Los modelos que requieran ser serializados de un mapa deben tener un constructor factory de nombre fromMap
que crea una instancia del modelo a partir de un objeto Map
.
A. Argumento data
El método fromMap
debe recibir un objeto de tipo Map<E,S>
como argumento obligatorio llamado data
. Generalmente, E
es de tipo String
y S
es de tipo dynamic
, pero esto puede variar de un modelo a otro.
class UserModel extends User {
factory UserModel.fromMap(Map<String, dynamic> data) { ... }
}
B. Argumentos extra
El método fromMap
puede recibir argumentos adicionales a data
. El parámetro data
debe ser posicional y el resto de parámetros deben ser de tipo nombrado y pueden ser opcionales u obligatorios.
class UserModel extends User {
factory UserModel.fromMap(Map<String, dynamic> data, {String? extra}) {
...
}
}
C. Variables intermedias en constructores fromMap
Los constructores fromMap
deben utilizar variables intermedias para almacenar los valores de los parámetros del constructor.
class UserModel extends User {
factory UserModel.fromMap(Map<String, dynamic> data) {
// 👇 Variables intermedias
final id = data['id'];
final name = data['name'];
return UserModel(id: id, name: name);
}
}
D. Manejo de valores nulos de parámetros en constructores fromMap
Todos los parámetros provenientes del argumento data
deben evaluar y resolver posibles valores nulos, salvo aquellos valores anulables opcionales, o aquellos que requieran una manejo especial según la necesidad del proyecto.
class UserModel extends User {
factory UserModel.fromMap(Map<String, dynamic> data) {
final id = data['id'] ?? ''; // 👈 Manejo de valor nulo
final name = data['name'] ?? '';
return UserModel(id: id, name: name);
}
}
Constructor nombrado empty
Los modelos deben tener un constructor nombrado constante empty
que crea un modelo vacío con todos sus atributos con el valor mínimo representable de cada tipo de dato. Por ejemplo, un int
sería 0
, un String
una cadena vacía ''
, List
una lista vacía []
, etc.
El constructor empty
debe ser un constructor constante.
class ProductModel extends Product {
const ProductModel.empty() : super(
id: '',
price: 0.0,
photos: [],
status: ProductStatus.inactive
);
}
El constructor empty
en modelos se usa principalmente en las pruebas unitarias para crear fácilmente instancias del modelo vacías.
Atributos
Herencia de atributos de la entidad
Los modelos deben respetar los atributos heredados de la entidad correspondiente. No se debe definir atributos redundantes o repetidos con aquellos presentes en la entidad heredada.
A. Atributos adicionales no redundantes
Los modelos pueden definir atributos adicionales que no estén presentes en la entidad que no sean redundantes, entendiendose por redundantes aquellos atributos con un mismo significado que ya están presentes en la entidad.
class User {
User(String id);
final String id;
}
class UserModel extends User {
UserModel({
required super.id,
required String extra,
});
final String extra;
}
Atributo query
Los modelos de objetos de GraphQL deben tener una constante estática query
de tipo String
con los campos respectivos del query que se debe ejecutar para obtener el modelo.
class UserModel extends User {
UserModel({
required super.id,
required super.name,
});
}
static const String query = r'''
id,
name,
''';
Atributos de tipo DateTime
Los modelos con atributos de tipo DateTime
deben declarar estos atributos como getters y guardar el valor en formato Unix epoch UTC (de tipo int
).
class UserModel extends User {
const UserModel({
required this.createdAtUnix,
});
const ProductModel.empty() : super(
createdAtUnix: 0,
);
DateTime get createdAt => DateTime.fromMillisecondsSinceEpoch(createdAtUnix);
final int createdAtUnix;
}
Esta regla también aplica a las entidades. En la mayoría de los casos, el modelo no necesita declarar el atributo dentro de su clase, ya que el valor se obtiene de la entidad por herencia.
Métodos especiales
Método toMap
Los modelos que requieran ser convertidos a un mapa deben tener un método toMap
que convierta el modelo en un objeto de tipo Map<String,dynamic>
.
class UserModel extends User {
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
};
}
}
A. Serialización de enums
Los modelo con atributos de tipo enum deben serializar los valores de los enums en el formato adecuado para el servicio externo usando las funciones auxiliares del enum modelo.
enum UserStatusModel {
active('active'),
inactive('inactive');
const UserStatusModel(this.value);
final String value;
UserStatusModel fromEntity(UserStatus status) {
switch (status) {
case UserStatus.active:
return UserStatusModel.active;
case UserStatus.inactive:
return UserStatusModel.inactive;
}
}
}
class UserModel extends User {
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'status': UserstatusModel.fromEntity(status).value, // 👈 Conversión del enum entidad al enum model
};
}
}
B. Omisión del método toMap
Los modelos que no requieran serializar a un mapa deben omitir el método toMap
.
Método toJson
Los modelos que requieran serializar a un objeto JSON deben tener un método toJson
que convierta el modelo en un String
en formato JSON mediante la función jsonEncode
de la librería dart:convert
.
import 'dart:convert';
class UserModel extends User {
Map<String, dynamic> toMap() { ... }
String toJson() {
return jsonEncode(toMap());
}
}
A. Omisión del método toJson
Los modelos que no requieran serializar a un objeto JSON deben omitir el método toJson
.
Se recomienda usar interfaces para forzar la estructura de los enumeradores.