419 lines
13 KiB
Markdown
419 lines
13 KiB
Markdown
|
|
# 🏗️ Separación de Capas y Organización de Lógica de Negocio
|
||
|
|
|
||
|
|
## 🎯 Objetivo
|
||
|
|
|
||
|
|
Estudiar y definir cómo separar las capas de la aplicación y organizar la lógica de negocio siguiendo Clean Architecture.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📐 Arquitectura en Capas
|
||
|
|
|
||
|
|
### Estructura Propuesta
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ PRESENTATION LAYER (Routes) │
|
||
|
|
│ - Express Routes │
|
||
|
|
│ - Middleware (auth, validation, rate-limit) │
|
||
|
|
│ - Request/Response DTOs │
|
||
|
|
│ - NO lógica de negocio │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ APPLICATION LAYER (Use Cases) │
|
||
|
|
│ - Services (orquestación) │
|
||
|
|
│ - Use Cases (casos de uso específicos) │
|
||
|
|
│ - DTOs de aplicación │
|
||
|
|
│ - Validaciones de aplicación │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ DOMAIN LAYER (Core) │
|
||
|
|
│ - Entities (entidades de negocio) │
|
||
|
|
│ - Value Objects (objetos de valor) │
|
||
|
|
│ - Domain Services (lógica de dominio pura) │
|
||
|
|
│ - Repository Interfaces (contratos) │
|
||
|
|
│ - Domain Events │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ INFRASTRUCTURE LAYER (Implementación) │
|
||
|
|
│ - Repository Implementations │
|
||
|
|
│ - Database Access │
|
||
|
|
│ - File Storage │
|
||
|
|
│ - External Services │
|
||
|
|
│ - Cache │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📁 Organización de Carpetas
|
||
|
|
|
||
|
|
```
|
||
|
|
backend/src/
|
||
|
|
├── domain/ # 🎯 DOMAIN LAYER
|
||
|
|
│ ├── entities/ # Entidades de negocio
|
||
|
|
│ │ ├── ContentItem.ts
|
||
|
|
│ │ ├── Drug.ts
|
||
|
|
│ │ ├── Protocol.ts
|
||
|
|
│ │ ├── GlossaryTerm.ts
|
||
|
|
│ │ └── MedicalReview.ts
|
||
|
|
│ │
|
||
|
|
│ ├── value-objects/ # Objetos de valor inmutables
|
||
|
|
│ │ ├── ContentStatus.ts
|
||
|
|
│ │ ├── ContentPriority.ts
|
||
|
|
│ │ ├── DoseRange.ts
|
||
|
|
│ │ ├── PatientAge.ts
|
||
|
|
│ │ ├── PatientWeight.ts
|
||
|
|
│ │ └── Version.ts
|
||
|
|
│ │
|
||
|
|
│ ├── services/ # Servicios de dominio
|
||
|
|
│ │ ├── CriticalErrorDetector.ts
|
||
|
|
│ │ ├── DoseCalculator.ts
|
||
|
|
│ │ └── ProtocolValidator.ts
|
||
|
|
│ │
|
||
|
|
│ ├── repositories/ # Interfaces de repositorios
|
||
|
|
│ │ ├── IContentRepository.ts
|
||
|
|
│ │ ├── IDrugRepository.ts
|
||
|
|
│ │ ├── IGlossaryRepository.ts
|
||
|
|
│ │ └── IMediaRepository.ts
|
||
|
|
│ │
|
||
|
|
│ └── events/ # Eventos de dominio
|
||
|
|
│ ├── ContentSubmitted.ts
|
||
|
|
│ ├── ContentApproved.ts
|
||
|
|
│ └── ContentPublished.ts
|
||
|
|
│
|
||
|
|
├── application/ # 🔧 APPLICATION LAYER
|
||
|
|
│ ├── services/ # Servicios de aplicación
|
||
|
|
│ │ ├── ContentService.ts
|
||
|
|
│ │ ├── DrugService.ts
|
||
|
|
│ │ ├── DoseValidationService.ts
|
||
|
|
│ │ ├── ProtocolValidationService.ts
|
||
|
|
│ │ ├── MedicalValidationService.ts
|
||
|
|
│ │ └── MediaService.ts
|
||
|
|
│ │
|
||
|
|
│ ├── use-cases/ # Casos de uso específicos
|
||
|
|
│ │ ├── content/
|
||
|
|
│ │ │ ├── CreateContentUseCase.ts
|
||
|
|
│ │ │ ├── UpdateContentUseCase.ts
|
||
|
|
│ │ │ ├── SubmitForReviewUseCase.ts
|
||
|
|
│ │ │ └── PublishContentUseCase.ts
|
||
|
|
│ │ │
|
||
|
|
│ │ ├── drugs/
|
||
|
|
│ │ │ ├── CalculateDoseUseCase.ts
|
||
|
|
│ │ │ ├── ValidateDoseUseCase.ts
|
||
|
|
│ │ │ └── CreateDrugUseCase.ts
|
||
|
|
│ │ │
|
||
|
|
│ │ └── protocols/
|
||
|
|
│ │ ├── ExecuteProtocolUseCase.ts
|
||
|
|
│ │ └── ValidateProtocolSequenceUseCase.ts
|
||
|
|
│ │
|
||
|
|
│ └── dto/ # Data Transfer Objects
|
||
|
|
│ ├── CreateContentDTO.ts
|
||
|
|
│ ├── UpdateContentDTO.ts
|
||
|
|
│ └── DoseCalculationDTO.ts
|
||
|
|
│
|
||
|
|
├── infrastructure/ # 🔌 INFRASTRUCTURE LAYER
|
||
|
|
│ ├── repositories/ # Implementaciones de repositorios
|
||
|
|
│ │ ├── ContentRepository.ts
|
||
|
|
│ │ ├── DrugRepository.ts
|
||
|
|
│ │ ├── GlossaryRepository.ts
|
||
|
|
│ │ └── MediaRepository.ts
|
||
|
|
│ │
|
||
|
|
│ ├── database/ # Acceso a base de datos
|
||
|
|
│ │ ├── Database.ts
|
||
|
|
│ │ ├── migrations/
|
||
|
|
│ │ └── queries/
|
||
|
|
│ │
|
||
|
|
│ ├── storage/ # Almacenamiento de archivos
|
||
|
|
│ │ ├── FileStorage.ts
|
||
|
|
│ │ └── MediaStorage.ts
|
||
|
|
│ │
|
||
|
|
│ ├── cache/ # Sistema de caché
|
||
|
|
│ │ └── CacheService.ts
|
||
|
|
│ │
|
||
|
|
│ └── external/ # Servicios externos
|
||
|
|
│ └── NotificationService.ts
|
||
|
|
│
|
||
|
|
└── presentation/ # 🌐 PRESENTATION LAYER
|
||
|
|
├── routes/ # Rutas Express
|
||
|
|
│ ├── content.ts
|
||
|
|
│ ├── drugs.ts
|
||
|
|
│ ├── glossary.ts
|
||
|
|
│ ├── media.ts
|
||
|
|
│ └── validation.ts
|
||
|
|
│
|
||
|
|
├── middleware/ # Middleware
|
||
|
|
│ ├── auth.ts
|
||
|
|
│ ├── validate.ts
|
||
|
|
│ └── rate-limit.ts
|
||
|
|
│
|
||
|
|
└── validators/ # Validadores Zod
|
||
|
|
├── content.ts
|
||
|
|
├── drugs.ts
|
||
|
|
└── glossary.ts
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔄 Flujo de Datos
|
||
|
|
|
||
|
|
### Ejemplo: Crear Contenido
|
||
|
|
|
||
|
|
```
|
||
|
|
1. Request → Route (presentation/routes/content.ts)
|
||
|
|
↓
|
||
|
|
2. Validar con Zod (presentation/validators/content.ts)
|
||
|
|
↓
|
||
|
|
3. Llamar Use Case (application/use-cases/content/CreateContentUseCase.ts)
|
||
|
|
↓
|
||
|
|
4. Use Case llama Service (application/services/ContentService.ts)
|
||
|
|
↓
|
||
|
|
5. Service usa Domain Entities (domain/entities/ContentItem.ts)
|
||
|
|
↓
|
||
|
|
6. Service llama Repository Interface (domain/repositories/IContentRepository.ts)
|
||
|
|
↓
|
||
|
|
7. Repository Implementation (infrastructure/repositories/ContentRepository.ts)
|
||
|
|
↓
|
||
|
|
8. Database (infrastructure/database/Database.ts)
|
||
|
|
↓
|
||
|
|
9. Response ← Route
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📝 Reglas de Separación
|
||
|
|
|
||
|
|
### ✅ Domain Layer
|
||
|
|
- **SÍ:** Entidades, Value Objects, Lógica de negocio pura
|
||
|
|
- **SÍ:** Interfaces de repositorios
|
||
|
|
- **NO:** Acceso a base de datos
|
||
|
|
- **NO:** Dependencias externas
|
||
|
|
- **NO:** Frameworks (Express, etc.)
|
||
|
|
|
||
|
|
### ✅ Application Layer
|
||
|
|
- **SÍ:** Casos de uso, Orquestación
|
||
|
|
- **SÍ:** Validaciones de aplicación
|
||
|
|
- **SÍ:** DTOs de aplicación
|
||
|
|
- **NO:** Lógica de dominio (debe estar en Domain)
|
||
|
|
- **NO:** Acceso directo a base de datos
|
||
|
|
|
||
|
|
### ✅ Infrastructure Layer
|
||
|
|
- **SÍ:** Implementaciones de repositorios
|
||
|
|
- **SÍ:** Acceso a base de datos
|
||
|
|
- **SÍ:** Servicios externos
|
||
|
|
- **NO:** Lógica de negocio
|
||
|
|
- **NO:** Validaciones de dominio
|
||
|
|
|
||
|
|
### ✅ Presentation Layer
|
||
|
|
- **SÍ:** Routes, Middleware
|
||
|
|
- **SÍ:** Validación de entrada (Zod)
|
||
|
|
- **SÍ:** Transformación Request/Response
|
||
|
|
- **NO:** Lógica de negocio
|
||
|
|
- **NO:** Acceso directo a repositorios
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 Ejemplo de Implementación
|
||
|
|
|
||
|
|
### Domain Entity (Inmutable)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// domain/entities/ContentItem.ts
|
||
|
|
export class ContentItem {
|
||
|
|
private constructor(
|
||
|
|
readonly id: string,
|
||
|
|
readonly type: ContentType,
|
||
|
|
readonly title: string,
|
||
|
|
readonly status: ContentStatus,
|
||
|
|
readonly createdAt: Date,
|
||
|
|
readonly updatedAt: Date
|
||
|
|
) {}
|
||
|
|
|
||
|
|
static create(
|
||
|
|
id: string,
|
||
|
|
type: ContentType,
|
||
|
|
title: string
|
||
|
|
): ContentItem {
|
||
|
|
// Validaciones de dominio
|
||
|
|
if (!title || title.trim().length === 0) {
|
||
|
|
throw new Error('Título es obligatorio');
|
||
|
|
}
|
||
|
|
|
||
|
|
return new ContentItem(
|
||
|
|
id,
|
||
|
|
type,
|
||
|
|
title,
|
||
|
|
ContentStatus.DRAFT,
|
||
|
|
new Date(),
|
||
|
|
new Date()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
submitForReview(): ContentItem {
|
||
|
|
if (this.status !== ContentStatus.DRAFT) {
|
||
|
|
throw new Error('Solo contenido en borrador puede enviarse a revisión');
|
||
|
|
}
|
||
|
|
|
||
|
|
return new ContentItem(
|
||
|
|
this.id,
|
||
|
|
this.type,
|
||
|
|
this.title,
|
||
|
|
ContentStatus.IN_REVIEW,
|
||
|
|
this.createdAt,
|
||
|
|
new Date()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Inmutable: siempre retorna nueva instancia
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Use Case
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// application/use-cases/content/CreateContentUseCase.ts
|
||
|
|
export class CreateContentUseCase {
|
||
|
|
constructor(
|
||
|
|
private readonly contentRepository: IContentRepository,
|
||
|
|
private readonly validator: ContentValidator
|
||
|
|
) {}
|
||
|
|
|
||
|
|
async execute(input: CreateContentDTO): Promise<ContentItem> {
|
||
|
|
// 1. Validar entrada
|
||
|
|
this.validator.validateCreate(input);
|
||
|
|
|
||
|
|
// 2. Crear entidad de dominio
|
||
|
|
const content = ContentItem.create(
|
||
|
|
crypto.randomUUID(),
|
||
|
|
input.type,
|
||
|
|
input.title
|
||
|
|
);
|
||
|
|
|
||
|
|
// 3. Persistir
|
||
|
|
await this.contentRepository.save(content);
|
||
|
|
|
||
|
|
// 4. Retornar
|
||
|
|
return content;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Repository Interface (Domain)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// domain/repositories/IContentRepository.ts
|
||
|
|
export interface IContentRepository {
|
||
|
|
findById(id: string): Promise<ContentItem | null>;
|
||
|
|
findAll(filters: ContentFilters): Promise<ContentItem[]>;
|
||
|
|
save(content: ContentItem): Promise<ContentItem>;
|
||
|
|
delete(id: string): Promise<void>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Repository Implementation (Infrastructure)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// infrastructure/repositories/ContentRepository.ts
|
||
|
|
export class ContentRepository implements IContentRepository {
|
||
|
|
constructor(private readonly db: Database) {}
|
||
|
|
|
||
|
|
async findById(id: string): Promise<ContentItem | null> {
|
||
|
|
const result = await this.db.query(
|
||
|
|
'SELECT * FROM tes_content.content_items WHERE id = $1',
|
||
|
|
[id]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (result.rows.length === 0) return null;
|
||
|
|
|
||
|
|
return this.mapToDomain(result.rows[0]);
|
||
|
|
}
|
||
|
|
|
||
|
|
async save(content: ContentItem): Promise<ContentItem> {
|
||
|
|
// Mapear de dominio a persistencia
|
||
|
|
const row = this.mapToPersistence(content);
|
||
|
|
|
||
|
|
await this.db.query(
|
||
|
|
`INSERT INTO tes_content.content_items (...) VALUES (...)`,
|
||
|
|
[row]
|
||
|
|
);
|
||
|
|
|
||
|
|
return content;
|
||
|
|
}
|
||
|
|
|
||
|
|
private mapToDomain(row: any): ContentItem {
|
||
|
|
// Mapear de persistencia a dominio
|
||
|
|
}
|
||
|
|
|
||
|
|
private mapToPersistence(content: ContentItem): any {
|
||
|
|
// Mapear de dominio a persistencia
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Route (Presentation)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// presentation/routes/content.ts
|
||
|
|
router.post(
|
||
|
|
'/',
|
||
|
|
authenticate,
|
||
|
|
requirePermission('content:write'),
|
||
|
|
validateBody(createContentSchema),
|
||
|
|
async (req: AuthRequest, res: Response) => {
|
||
|
|
try {
|
||
|
|
const useCase = new CreateContentUseCase(
|
||
|
|
contentRepository,
|
||
|
|
contentValidator
|
||
|
|
);
|
||
|
|
|
||
|
|
const content = await useCase.execute(req.body);
|
||
|
|
|
||
|
|
res.status(201).json({
|
||
|
|
id: content.id,
|
||
|
|
title: content.title,
|
||
|
|
status: content.status.toString()
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
handleError(error, res);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔒 Reglas de Dependencias
|
||
|
|
|
||
|
|
### Regla de Dependencia
|
||
|
|
- **Domain:** No depende de nadie
|
||
|
|
- **Application:** Solo depende de Domain
|
||
|
|
- **Infrastructure:** Depende de Domain y Application
|
||
|
|
- **Presentation:** Depende de Application y Domain
|
||
|
|
|
||
|
|
### Diagrama de Dependencias
|
||
|
|
|
||
|
|
```
|
||
|
|
Presentation → Application → Domain
|
||
|
|
↓ ↓
|
||
|
|
Infrastructure → Domain
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ✅ Checklist de Separación
|
||
|
|
|
||
|
|
Al crear nuevo código, verificar:
|
||
|
|
|
||
|
|
- [ ] ¿Está en la capa correcta?
|
||
|
|
- [ ] ¿Depende solo de capas inferiores?
|
||
|
|
- [ ] ¿La lógica de negocio está en Domain?
|
||
|
|
- [ ] ¿Los casos de uso están en Application?
|
||
|
|
- [ ] ¿Las implementaciones están en Infrastructure?
|
||
|
|
- [ ] ¿Las rutas están en Presentation?
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Fin del documento**
|