¿Métodos en la entidad, o fuera?
De las 13 entidades de petid-api, siete tienen métodos y seis no. No es descuido: el corte es
deliberado y bastante nítido.
| Con comportamiento | Datos puros |
|---|---|
Session · isActive(), isRevoked() |
Device |
PetTag · isActive, canNotify() |
Diagnosis |
ContactRequest · isValid(), isValidFor() |
Profile |
Pet · assertAccessibleBy() |
Report |
User · isEmailVerified(), isAdmin() |
Resource |
Tenant · isDeleted() |
Settings |
Appointment · isPending |
La regla
Método en la entidad cuando la respuesta depende de varios campos suyos, o del tiempo, y la pregunta se repite. Si es
entity.campo === X, va en quien pregunta.
Mira los tres casos que más claramente la cumplen:
// Depende del tiempo, y encapsula un número que no debería andar suelto
canNotify(now: Date): boolean {
if (!this.lastNotifiedAt) return true;
return now.getTime() - this.lastNotifiedAt.getTime() > NOTIFY_COOLDOWN_MS;
}
// Combina tres condiciones que nadie debería recomponer a mano
isValidFor(userId: string, hashedCode: string, now: Date): boolean
// Es la regla de acceso entera, en un único sitio
assertAccessibleBy(requester: User): void
canNotify es el mejor ejemplo de por qué merece la pena: la ventana de 10 minutos es una regla de
negocio. Si viviera en el use case, el día que cambie hay que buscarla; viviendo en la entidad, hay
un sitio y el nombre lo explica.
Y ahora el contraste: Report no tiene ni un método. No porque sea menos importante —es la
entidad central del historial clínico— sino porque no hay ninguna pregunta sobre un reporte cuya
respuesta no sea leer un campo. Añadirle métodos por simetría sería ruido.
Dos matices que se ven en el código
La entidad decide, pero no busca. Nunca carga nada ni llama a un repositorio. Quien la carga es otro:
// application/access/pet-access.service.ts
const pet = await this.pets.findById(petId); // el service busca
if (!pet) throw new PetNotFoundError(petId); // el service traduce el vacío
pet.assertAccessibleBy(requester); // la entidad decide
assert* frente a is*. Dos formas distintas y no son intercambiables:
is*/can*devuelvenboolean. Quien pregunta decide qué hacer.assert*no devuelve nada y lanza el error de dominio correspondiente. Se usa cuando seguir adelante sin cumplir la condición es sencillamente incorrecto, así que no tiene sentido dejar la decisión al llamante.
Si te encuentras escribiendo if (!pet.isAccessibleBy(user)) throw new ... en varios sitios, eso
quería ser un assert.
Cómo son las entidades por dentro
export class PetTag {
readonly id: string;
readonly publicCode: string;
readonly revokedAt: Date | null;
constructor(p: { id: string; publicCode: string; revokedAt: Date | null /* ... */ }) {
this.id = p.id;
// ...
}
}
- Campos
readonlyy constructor con un objeto, no con 8 parámetros posicionales. - Sin decoradores, sin imports de librerías. Una entidad es TypeScript y nada más.
- Las constantes de negocio viven arriba del fichero (
const NOTIFY_COOLDOWN_MS = ...), no esparcidas por los use cases.