Cuando MVVM y MVC dejan de salvar, y los tests se convierten en un infierno
¿Alguna vez has escrito una aplicación en Swift donde después de seis meses agregar una funcionalidad rompía otras tres, y tenías que reescribir los tests por completo porque la base de datos cambió de Core Data a Realm? Si es así, no estás solo. Las arquitecturas clásicas en capas (MVC, MVVM) funcionan muy bien al inicio, pero se convierten en un "batido" a medida que el proyecto crece.
Te mostraré la Arquitectura Hexagonal (también conocida como Ports & Adapters) — un enfoque que viene del mundo de Java y DDD, pero que se adapta perfectamente a Swift. La analizaremos con ejemplos reales, sin "User" ni "Manager" abstractos. Verás cómo separar la lógica de negocio de la base de datos, la UI y la red, de modo que puedas cambiarlas sin tocar el núcleo de la aplicación.
Esto no es otra teoría — es práctica que podrás aplicar en tu proyecto hoy mismo.
¿Qué es la Arquitectura Hexagonal? En palabras simples
Imagina un hexágono (de ahí el nombre). En el centro está tu lógica de negocio pura (dominio). En los bordes están los puertos (interfaces) a través de los cuales el mundo exterior se comunica con el núcleo. Los adaptadores son las implementaciones concretas: base de datos, API, UI, sistema de archivos.
La regla principal: el núcleo no sabe nada del mundo exterior. Ni de UIKit, ni de URLSession, ni de Core Data. Solo conoce sus interfaces (puertos).
Ejemplo de la vida real: imagina una tienda en línea. El núcleo sabe que hay un "Carrito" y "Finalizar pedido". No le importa de dónde vienen los datos — si de un websocket, de un JSON local o del teclado. Los adaptadores se conectan a los puertos y traducen las señales externas en comandos de dominio.
¿Por qué deberías usarla en proyectos Swift?
- Testeabilidad — pruebas el dominio sin base de datos ni red reales. Simplemente reemplazas los adaptadores con mocks.
- Flexibilidad — ¿quieres migrar de Firebase a tu propio servidor? Cambias un adaptador, no la mitad de la aplicación.
- Legibilidad — la lógica de negocio no está dispersa por los ViewController. Está en un solo lugar.
- Independencia de frameworks — tu núcleo no importa UIKit, Combine ni Alamofire.
Estructura del proyecto con Arquitectura Hexagonal en Swift
Vayamos directo al código. Supongamos que estamos escribiendo una aplicación para gestionar tareas (Todolist). Así se vería la estructura de carpetas:
App/├── Domain/│ ├── Entity/│ │ └── Task.swift│ ├── UseCase/│ │ └── CreateTaskUseCase.swift│ └── Port/│ ├── TaskRepository.swift│ └── TaskOutputPort.swift├── Adapter/│ ├── Persistence/│ │ └── CoreDataTaskRepository.swift│ ├── Network/│ │ └── APITaskRepository.swift│ └── Presentation/│ └── TaskListViewModel.swift└── Main/ └── AppDelegate.swiftObserva: Domain no importa nada excepto Foundation. Adapter puede importar CoreData, UIKit, Alamofire — lo que sea.
Paso 1: Entidades de dominio y puertos
Empecemos con lo más importante — el modelo de dominio. Es un struct o enum que describe la entidad de negocio. Nada de NSManagedObject, nada de Codable.
// Domain/Entity/Task.swiftstruct Task { let id: UUID var title: String var isCompleted: Bool let createdAt: Date}Ahora definimos el puerto para el repositorio (puerto de salida). Es un protocolo que describe cómo guardamos y cargamos tareas.
// Domain/Port/TaskRepository.swiftprotocol TaskRepository { func fetchAll() async throws -> [Task] func save(task: Task) async throws func delete(taskID: UUID) async throws}Y el puerto de entrada — a través del cual el mundo exterior (UI, API) interactúa con el núcleo. Generalmente es un UseCase.
// Domain/Us