En este momento estás viendo Arquitectura Limpia (Clean Architecture) y Hexagonal en Ángular

Arquitectura Limpia (Clean Architecture) y Hexagonal en Ángular

Implementar una arquitectura Limpia (Clean Architecture) y Hexagonal en Angular implica estructurar el proyecto para separar claramente la lógica de negocio de las implementaciones técnicas (UI, APIs, almacenamiento) y garantizar que el dominio sea el núcleo independiente del framework. Aquí te explico cómo hacerlo de forma práctica:

  • Arquitectura Limpia: Propuesta por Robert C. Martin, se basa en la separación de responsabilidades en capas (presentación, lógica de negocio y datos) y en la regla de dependencia: las capas internas no deben depender de las externas.
  • Arquitectura Hexagonal: Propuesta por Alistair Cockburn, se centra en separar la lógica de negocio de las interfaces externas (como APIs, bases de datos o UI), utilizando puertos (interfaces) y adaptadores.

1. Principios Clave

  1. Dominio como núcleo: El dominio (entidades y reglas de negocio) no depende de nada externo (ni Angular, ni APIs).
  2. Puertos y Adaptadores:
    • Puertos: Interfaces que definen cómo interactúa el dominio con el exterior (ej: UserRepository).
    • Adaptadores: Implementaciones concretas de esos puertos (ej: un servicio de Angular que consume una API).
  3. Inversión de Dependencias: Las capas externas (UI, infraestructura) dependen de las internas (dominio), no al revés.

    2. Estructura de Carpetas Recomendada

    Organiza tu proyecto en capas claras:

    src/ ├── app/ │ ├── domain/ # Capa de Dominio (núcleo) │ │ ├── models/ # Entidades y objetos de valor (User, Product) │ │ ├── ports/ # Interfaces (UserRepository, AuthService) │ │ └── use-cases/ # Casos de uso (GetUserList, CreateOrder) │ │ │ ├── infrastructure/ # Capa de Infraestructura (implementaciones) │ │ ├── adapters/ # Adaptadores (API calls, repositorios) │ │ └── services/ # Servicios técnicos (HTTP, localStorage) │ │ │ ├── presentation/ # Capa de Presentación (UI) │ │ ├── components/ # Componentes de Angular │ │ ├── pages/ # Vistas/páginas │ │ └── state/ # Gestión de estado (NgRx, servicios reactivos) │ │ │ └── shared/ # Utilidades compartidas (pipes, directivas) └── ...

    3. Implementación Paso a Paso

    a. Capa de Dominio (domain/)

    • Entidades: Clases simples que representan los datos del negocio.
     // domain/models/user.model.ts export class User { constructor( public id: string, public name: string, public email: string ) {} }
    • Puertos: Interfaces que definen contratos para interactuar con el exterior.
     // domain/ports/user.repository.ts export interface UserRepository { getUsers(): Observable<User[]>; createUser(user: User): Observable<void>; }
    • Casos de Uso: Clases que encapsulan la lógica de negocio.
     // domain/use-cases/get-users.use-case.ts @Injectable({ providedIn: 'root' }) export class GetUsersUseCase { constructor(private userRepository: UserRepository) {} execute(): Observable<User[]> { return this.userRepository.getUsers(); } }

    b. Capa de Infraestructura (infrastructure/)

    • Adaptadores: Implementan los puertos del dominio usando servicios técnicos (HTTP, etc.).
     // infrastructure/adapters/user-api.adapter.ts @Injectable({ providedIn: 'root' }) export class UserApiAdapter implements UserRepository { constructor(private http: HttpClient) {} getUsers(): Observable<User[]> { return this.http.get<User[]>('/api/users'); } }

    c. Capa de Presentación (presentation/)

    • Componentes: Solo manejan la UI y delegan la lógica a los casos de uso.
     // presentation/pages/user-list/user-list.component.ts @Component({ /* ... */ }) export class UserListComponent { users$: Observable<User[]>; constructor(private getUsersUseCase: GetUsersUseCase) { this.users$ = this.getUsersUseCase.execute(); } }

    4. Configuración de Dependencias

    • Registra los adaptadores en Angular para que los casos de uso reciban las implementaciones correctas:
     // app.module.ts @NgModule({ providers: [ { provide: UserRepository, useClass: UserApiAdapter }, // Inversión de dependencias GetUsersUseCase, ], }) export class AppModule {}

    5. Flujo de Datos Típico

    1. Componente llama a un caso de uso.
    2. Caso de uso usa un puerto (interfaz) para acceder a datos.
    3. Adaptador (ej: UserApiAdapter) implementa el puerto y consume una API.
    4. Los datos fluyen de vuelta al componente a través de observables o promesas.

    6. Beneficios Clave

    • Testabilidad: Puedes mockear fácilmente los puertos en pruebas unitarias.
     // Ejemplo de test const mockUserRepository: UserRepository = { getUsers: () => of([new User('1', 'John', 'john@test.com')]), }; it('should get users', () => { const useCase = new GetUsersUseCase(mockUserRepository); useCase.execute().subscribe(users => { expect(users.length).toBe(1); }); });
    • Flexibilidad: Cambiar una API o framework (ej: React en lugar de Angular) no afecta al dominio.
    • Mantenibilidad: Cada capa tiene responsabilidades claras.

    7. Herramientas Recomendadas

    • RxJS: Para manejar flujos de datos reactivos.
    • NgRx/Akita: Si necesitas gestión de estado compleja.
    • Jest/Cypress: Para pruebas unitarias y E2E.

    Ejemplo en Código

    // Componente usando un caso de uso @Component({ selector: 'app-user-list', template: ` <ul> <li *ngFor="let user of users$ | async">{{ user.name }}</li> </ul> `, }) export class UserListComponent { users$: Observable<User[]>; constructor(private getUsersUseCase: GetUsersUseCase) { this.users$ = this.getUsersUseCase.execute(); } }

    Conclusión

    Esta arquitectura puede parecer “over-engineering” para proyectos pequeños, pero es invaluable en aplicaciones complejas. La clave es:

    1. Aislar el dominio de Angular y las APIs.
    2. Usar interfaces (puertos) para definir contratos.
    3. Inyectar adaptadores para cumplir con esos contratos.

    Si mantienes estas reglas, tu código será más limpio, mantenible y resistente a cambios en tecnologías externas.

    Deja un comentario