Saltar al contenido principal

Capa de Dominio: Entidades y Casos de Uso

La Capa de Dominio es el núcleo de cualquier aplicación construida bajo Clean Architecture. En esta capa residen las entidades y las reglas de negocio más estables del sistema.

Una característica fundamental en el frontend es que el Dominio está escrito en TypeScript 100% puro. No depende de React, no conoce ganchos (useState), no sabe qué es Next.js ni realiza llamadas directas a través de fetch o axios.


1. La Cebolla Arquitectónica en el Frontend​

La estructura concéntrica de Clean Architecture sitúa al Dominio en el centro absoluto, rodeado por los Casos de Uso y finalmente por los adaptadores externos:

La Cebolla de Clean Architecture en Frontend

La Regla de Dependencia en Acción​

  • La capa de presentación (React) conoce a los casos de uso.
  • Los casos de uso conocen a las entidades y a los contratos de repositorio.
  • Las entidades y contratos de repositorio no conocen a nadie por fuera del dominio.

2. Modelado de Entidades de Dominio​

Una Entidad representa un concepto central del negocio con identidad propia e invariantes que deben cumplirse en todo momento.

En nuestra feature de fitness, el modelo Exercise define los atributos inmutables requeridos y las restricciones del dominio:

src/features/exercises/domain/entities/exercise.ts
export type MuscleGroup = 'CHEST' | 'BACK' | 'LEGS' | 'SHOULDERS' | 'ARMS' | 'CORE';
export type Difficulty = 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED';

export interface ExerciseProps {
readonly id: string;
readonly name: string;
readonly description: string;
readonly muscleGroup: MuscleGroup;
readonly difficulty: Difficulty;
readonly createdAt: Date;
}

export class Exercise {
readonly id: string;
readonly name: string;
readonly description: string;
readonly muscleGroup: MuscleGroup;
readonly difficulty: Difficulty;
readonly createdAt: Date;

constructor(props: ExerciseProps) {
this.validate(props);
this.id = props.id;
this.name = props.name;
this.description = props.description;
this.muscleGroup = props.muscleGroup;
this.difficulty = props.difficulty;
this.createdAt = props.createdAt;
}

/**
* Invariante de negocio: Un ejercicio no puede existir con datos incompletos
*/
private validate(props: ExerciseProps): void {
if (!props.name || props.name.trim().length < 3) {
throw new Error('El nombre del ejercicio debe tener al menos 3 caracteres.');
}

if (!props.description || props.description.trim().length < 10) {
throw new Error('La descripción debe tener al menos 10 caracteres explicativos.');
}
}

/**
* Método de negocio: Determina si el ejercicio es adecuado para principiantes
*/
isBeginnerFriendly(): boolean {
return this.difficulty === 'BEGINNER';
}
}

Características de una Entidad Limpia​

  1. Inmutabilidad: Todas las propiedades son de solo lectura (readonly). Modificar un objeto genera una nueva instancia, evitando efectos secundarios imprevistos en la interfaz de usuario.
  2. Tipado Estricto: Hacemos uso de tipos de unión (type MuscleGroup = 'CHEST' | ...) en lugar de string abiertos.
  3. Validación de Invariantes: Si los datos no cumplen las reglas del negocio, el constructor arroja una excepción antes de que el objeto inválido contamine la aplicación.

3. Contratos de Repositorio en el Dominio​

El dominio necesita persistir y consultar datos, pero no debe saber cómo ni de dónde se obtienen. Para lograrlo, define un contrato abstracto mediante una interface de TypeScript:

src/features/exercises/domain/repositories/exercise.repository.ts
import { Exercise } from '../entities/exercise';

export interface CreateExerciseParams {
name: string;
description: string;
muscleGroup: Exercise['muscleGroup'];
difficulty: Exercise['difficulty'];
}

export interface ExerciseRepository {
/**
* Obtiene la lista completa de ejercicios disponibles
*/
getAll(): Promise<Exercise[]>;

/**
* Obtiene un ejercicio por su identificador único
*/
getById(id: string): Promise<Exercise | null>;

/**
* Guarda un nuevo ejercicio y retorna la entidad persistida
*/
create(params: CreateExerciseParams): Promise<Exercise>;
}
Principio de Inversión de Dependencias (DIP)

Fíjate en que la interfaz se llama ExerciseRepository y está ubicada dentro de la carpeta domain/repositories/. No tiene mención a fetch, SQL, MongoDB ni Axios. El dominio define el contrato; la infraestructura se encargará de cumplirlo.


4. Casos de Uso (Interactors)​

Un Caso de Uso encapsula una intención específica del usuario en el sistema. Sigue el principio de responsabilidad única: una clase con un único método público execute().

Caso de Uso 1: Obtener Ejercicios​

src/features/exercises/domain/usecases/get-exercises.usecase.ts
import { Exercise } from '../entities/exercise';
import { ExerciseRepository } from '../repositories/exercise.repository';

export class GetExercisesUseCase {
constructor(private readonly repository: ExerciseRepository) {}

async execute(): Promise<Exercise[]> {
const exercises = await this.repository.getAll();

// Regla de aplicación: ordenar alfabéticamente por nombre
return exercises.sort((a, b) => a.name.localeCompare(b.name));
}
}

Caso de Uso 2: Crear un Nuevo Ejercicio​

src/features/exercises/domain/usecases/create-exercise.usecase.ts
import { Exercise } from '../entities/exercise';
import { CreateExerciseParams, ExerciseRepository } from '../repositories/exercise.repository';

export class CreateExerciseUseCase {
constructor(private readonly repository: ExerciseRepository) {}

async execute(params: CreateExerciseParams): Promise<Exercise> {
// 1. Sanitización de entradas
const sanitizedParams: CreateExerciseParams = {
name: params.name.trim(),
description: params.description.trim(),
muscleGroup: params.muscleGroup,
difficulty: params.difficulty,
};

// 2. Validación de reglas antes de delegar a la persistencia
if (sanitizedParams.name.length < 3) {
throw new Error('El nombre debe tener al menos 3 caracteres.');
}

// 3. Delegación al repositorio
return await this.repository.create(sanitizedParams);
}
}

5. El Gran Beneficio: Pruebas Unitarias Instantáneas​

Al no depender del DOM, de React ni de servidores HTTP, los casos de uso se pueden verificar con pruebas unitarias que corren en milisegundos en Node.js mediante un repositorio falso (Mock o Stub):

src/features/exercises/domain/usecases/__tests__/create-exercise.usecase.test.ts
import { CreateExerciseUseCase } from '../create-exercise.usecase';
import { ExerciseRepository, CreateExerciseParams } from '../../repositories/exercise.repository';
import { Exercise } from '../../entities/exercise';

describe('CreateExerciseUseCase', () => {
it('debe arrojar error si el nombre tiene menos de 3 caracteres', async () => {
// Repositorio falso en memoria sin necesidad de levantar Jest DOM ni backend
const mockRepo: ExerciseRepository = {
getAll: jest.fn(),
getById: jest.fn(),
create: jest.fn(),
};

const useCase = new CreateExerciseUseCase(mockRepo);

const invalidParams: CreateExerciseParams = {
name: 'Ab',
description: 'Descripción de prueba extensa',
muscleGroup: 'CORE',
difficulty: 'BEGINNER',
};

await expect(useCase.execute(invalidParams)).rejects.toThrow(
'El nombre debe tener al menos 3 caracteres.'
);
expect(mockRepo.create).not.toHaveBeenCalled();
});
});

6. Resumen de la Lección​

  1. La Capa de Dominio es agnóstica de frameworks e interfaces de usuario; se escribe en TypeScript puro.
  2. Las Entidades salvaguardan las invariantes del negocio mediante atributos inmutables y validaciones en el constructor.
  3. Los Repositorios en Dominio son únicamente contratos abstractos (interfaces), sin implementación técnica.
  4. Los Casos de Uso expresan intenciones directas del usuario (GetExercises, CreateExercise) y son triviales de testear de forma aislada.
  5. En la próxima sesión aprenderemos a implementar la Capa de Infraestructura, construyendo el patrón Repositorio concreto, los Datasources y los Mappers defensivos.

Cuestionario de Autoevaluación​

Cargando cuestionario...