Senior
¿Cómo resolver el problema del uso de dependencias de componentes periféricos en el núcleo?
sobes.tech AI
Respuesta de la IA
Resolver el problema del uso de dependencias de componentes periféricos (adaptadores, interfaces) en el núcleo (lógica de dominio) en arquitecturas como la Arquitectura Limpia o DDD, puede hacerse mediante el principio de Inversión de Dependencias (Dependency Inversion Principle - DIP).
La esencia del DIP:
- Los módulos de nivel superior no deben depender de los módulos de nivel inferior. Ambos deben depender de abstracciones.
- Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.
Aplicación práctica:
- El núcleo declara interfaces: El núcleo define abstracciones (interfaces o clases abstractas) que describen las funciones necesarias de los componentes periféricos.
- Los componentes periféricos implementan interfaces: Los módulos externos (adaptadores para bases de datos, clientes HTTP, sistemas de archivos, etc.) implementan estas interfaces.
- Inyección de dependencias (Dependency Injection - DI): Los componentes periféricos se inyectan en el núcleo desde fuera, generalmente en un nivel superior de la aplicación (por ejemplo, en la raíz de composición o usando un contenedor DI). El núcleo solo trabaja con abstracciones.
Ejemplo de uso de DIP en Node.js:
Supongamos que tenemos un núcleo que necesita acceso a datos.
// src/domain/interfaces/UserRepository.ts
// El núcleo declara la interfaz
export interface UserRepository {
getUserById(id: string): Promise<User | null>;
}
// src/domain/entities/User.ts
// Entidad del núcleo
export class User {
constructor(public id: string, public name: string) {}
}
// src/domain/usecases/GetUserUseCase.ts
// Caso de uso del núcleo depende de la abstracción UserRepository
import { UserRepository } from '../interfaces/UserRepository';
import { User } from '../entities/User';
export class GetUserUseCase {
constructor(private userRepository: UserRepository) {}
async execute(id: string): Promise<User | null> {
return this.userRepository.getUserById(id);
}
}
// src/infrastructure/database/MongoDbUserRepository.ts
// Componente periférico que implementa la interfaz del núcleo
import { UserRepository } from '../../domain/interfaces/UserRepository';
import { User } from '../../domain/entities/User';
export class MongoDbUserRepository implements UserRepository {
// Aquí irá la lógica de interacción con MongoDB
async getUserById(id: string): Promise<User | null> {
console.log(`Fetching user with id ${id} from MongoDB...`);
// Simulación de obtención de datos
if (id === '123') {
return new User('123', 'John Doe');
}
return null;
}
}
// src/app/CompositionRoot.ts
// La raíz de composición recoge las dependencias
import { GetUserUseCase } from '../domain/usecases/GetUserUseCase';
import { MongoDbUserRepository } from '../infrastructure/database/MongoDbUserRepository';
// Inyección de dependencia: El núcleo obtiene la implementación MongoDbUserRepository a través de la interfaz
const userRepository = new MongoDbUserRepository();
const getUserUseCase = new GetUserUseCase(userRepository);
// Ahora podemos usar el caso de uso
async function run() {
const user = await getUserUseCase.execute('123');
if (user) {
console.log(`Found user: ${user.name}`);
} else {
console.log('User not found');
}
}
run();
Ventajas de este enfoque:
- Independencia del núcleo: El núcleo no conoce las implementaciones específicas de los componentes periféricos.
- Testabilidad: El núcleo es fácil de probar, sustituyendo implementaciones mock de las interfaces.
- Flexibilidad: Se puede reemplazar fácilmente una implementación (por ejemplo, la base de datos) por otra, sin cambiar el código del núcleo.
- Mantenimiento: Se facilita la gestión y modificación del código.