En el diseño guiado por el dominio (Domain-Driven Design o DDD), nos encontramos con conceptos que no se definen por una identidad única, sino por el conjunto de sus atributos. Estos son los llamados Value Objects (Objetos de Valor).
¿Qué es un Value Object?
Un Value Object es un objeto que representa un concepto del dominio cuya importancia radica en lo que es y no en quién es. A diferencia de las Entidades, los Value Objects no tienen un identificador (ID).
Sus características principales son:
- Igualdad Estructural: Dos objetos de valor son iguales si todas sus propiedades coinciden.
- Inmutabilidad: Una vez creado, su estado no debería cambiar. Si necesitamos un «nuevo» valor, creamos una nueva instancia.
- Validación intrínseca: Un Value Object siempre debe representar un estado válido del dominio.
Análisis de la Implementación: DddValueObject<TValue>
La clase base DddValueObject en nuestra librería está diseñada para proporcionar un marco robusto y tipado para crear estos objetos.
¿Por qué esta implementación?
La implementación busca resolver varios retos técnicos comunes:
- Tipado Fuerte: Mediante el uso de genéricos (
TValue), aseguramos que el valor interno sea consistente. - Gestión de Reglas de Negocio: Integra un sistema de validación proactivo que impide la existencia de objetos con datos inválidos.
- Notificación de Cambios: Hereda de
AbstractNotifyPropertyChanged, lo que permite rastrear cambios.
Organización de la Clase
- Gestores de Estado y Reglas:
brokenRules,validatorRulesytrackingState. - Ciclo de Vida y Valor:
constructor,getValue()ysetValue(). - Lógica de Igualdad:
getEqualityComponents()yequals(). - Utilidades:
getHashCode()yclone().
Ejemplo Detallado: Email con Validación
Para implementar un Value Object de Email, no basta con guardar un string; debemos asegurar que el formato sea correcto y que el objeto sea inmutable.
import { DddValueObject, AbstractRuleValidator, BrokenRule } from '@nestjslatam/ddd-lib';
/**
* Validador de formato de Email (Regla de Negocio).
*/
export class EmailFormatValidator extends AbstractRuleValidator<Email> {
public validate(): BrokenRule[] {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(this.target.getValue())) {
return [{
property: 'Email',
message: 'El formato del correo electrónico no es válido'
}];
}
return [];
}
}
/**
* Representa una dirección de correo electrónico válida.
*/
export class Email extends DddValueObject<string> {
private constructor(value: string) {
super(value.toLowerCase().trim()); // Normalización inicial
}
public static create(value: string): Email {
return new Email(value);
}
public override addValidators(): void {
super.addValidators();
this.validatorRules.add(new EmailFormatValidator(this));
}
protected getEqualityComponents(): Iterable<any> {
return [this.getValue()];
}
}
Integración con class-validator
En proyectos NestJS, es común usar class-validator. Podemos integrar estas validaciones en nuestros Value Objects creando un validador genérico que use la librería.
Validador Universal de Clase
import { validateSync } from 'class-validator';
import { AbstractRuleValidator, BrokenRule, DddValueObject } from '@nestjslatam/ddd-lib';
export class ClassValidatorRule<T> extends AbstractRuleValidator<DddValueObject<T>> {
public validate(): BrokenRule[] {
const value = this.target.getValue();
// Si el valor es un objeto, ejecutamos la validación de sus decoradores
const errors = validateSync(value as object, { whitelist: true });
return errors.map(err => ({
property: err.property,
message: Object.values(err.constraints || {}).join(', ')
}));
}
}
Ejemplo Complejo: Address
import { IsString, IsNotEmpty, Length } from 'class-validator';
import { DddValueObject } from '@nestjslatam/ddd-lib';
export class AddressProps {
@IsString() @IsNotEmpty()
street: string;
@IsString() @IsNotEmpty()
city: string;
@Length(5, 5, { message: 'El código postal debe tener 5 dígitos' })
zipCode: string;
}
export class Address extends DddValueObject<AddressProps> {
private constructor(props: AddressProps) {
super(props);
}
public static create(street: string, city: string, zipCode: string): Address {
const props = new AddressProps();
props.street = street;
props.city = city;
props.zipCode = zipCode;
return new Address(props);
}
public override addValidators(): void {
super.addValidators();
this.validatorRules.add(new ClassValidatorRule(this));
}
protected getEqualityComponents(): Iterable<any> {
const v = this.getValue();
return [v.street, v.city, v.zipCode];
}
}
Verificación y Uso
const address = Address.create('Calle Falsa 123', 'Springfield', '123'); // CP inválido
if (!address.isValid) {
const errors = address.brokenRules.getBrokenRules();
console.log('Errores detectados:', errors.map(e => e.message));
}
Implementar Value Objects con validación fuerte garantiza que el «Estado Inválido» nunca se propague por tu sistema, manteniendo la integridad del modelo de dominio.