Por qué modularizar: compilación, fronteras y previews
La modularización no es orden estético sino una decisión de ingeniería con tres retornos medibles: builds incrementales que no recompilan el mundo, fronteras que una máquina verifica en vez de un revisor cansado, y previews que arrancan en segundos porque cargan una fracción del binario.
Casi todo el mundo llega a la modularización por el sitio equivocado: por estética. El proyecto tiene doscientos archivos en cuatro carpetas, alguien propone «vamos a organizarlo mejor» y el resultado es una jerarquía de carpetas más bonita que compila exactamente igual de lento. Las carpetas son etiquetas: no imponen nada, no impiden nada y el compilador ni siquiera las ve. Un módulo es otra cosa. Un módulo es una unidad de compilación con una superficie pública declarada, y esa diferencia —que parece administrativa— es la que convierte tres problemas crónicos de una app grande en problemas acotados: el tiempo que tardas en ver un cambio, la facilidad con la que una capa se cuela en otra, y el minuto y medio que tu preview tarda en dibujar un botón. Esta lección no explica cómo partir un proyecto; explica qué compras exactamente cuando lo haces, porque sin esa contabilidad la decisión se toma por moda y se abandona por cansancio.
- Distinguir una carpeta de un módulo en términos de lo que el compilador puede verificar.
- Explicar por qué un
targetmonolítico convierte cada edición en una recompilación amplia. - Relacionar el tamaño del módulo con el tiempo de arranque de un
preview. - Establecer las medidas que justifican o desmienten una modularización antes de emprenderla.
Una carpeta no es una frontera
En un target único de Xcode, todo el código comparte un mismo ámbito de módulo. El modificador internal, que es el nivel de acceso por defecto de Swift, significa «visible dentro del módulo», y si el módulo es la app entera entonces internal significa, en la práctica, público para todo el mundo. Tu capa de red puede instanciar una vista. Tu vista puede tocar el URLSession. El repositorio puede leer un @Environment. Nada de eso está prohibido porque no hay nada que lo prohíba: la única defensa es la disciplina del equipo, y la disciplina del equipo es una defensa que se degrada con cada incorporación, cada urgencia y cada viernes por la tarde.
Conviene notar que el problema no es que la gente quiera romper las capas. Es que en un target único no hay forma de saber que se están rompiendo. El autocompletado ofrece todos los símbolos del proyecto sin distinción de capa, el import no hace falta, y el código resultante compila y funciona. La transgresión no se comete: se descubre meses después, cuando alguien intenta extraer la feature y encuentra que arrastra media aplicación.
Esto no es un juicio moral sobre los programadores. Es una observación sobre el coste de la vigilancia. Una regla que solo vive en la cabeza de la gente y en los comentarios de las revisiones de código se aplica de forma probabilística; una regla codificada en la topología del proyecto se aplica siempre, sin discusión y sin coste humano recurrente. Cuando el módulo Dominio no declara dependencia sobre SwiftUI, no es que esté mal visto importar SwiftUI allí: es que el import no compila. La diferencia entre una convención y una restricción verificada es la diferencia entre una arquitectura que se documenta y una que existe.
El mismo argumento vale para el sentido inverso. Un módulo obliga a decidir qué es public. Ese acto —marcar explícitamente lo que sale— convierte el diseño de la interfaz en una decisión consciente y localizada en vez de un accidente acumulado. La superficie pública de un módulo se puede leer entera; la superficie efectiva de una carpeta dentro de un target monolítico es todo su contenido.
// Con un unico target: los cinco niveles de acceso colapsan en dos utiles
private // solo este ambito lexico
fileprivate // solo este archivo
internal // todo el proyecto, porque el proyecto es un modulo
public // identico a internal en la practica
open // solo relevante para herencia entre modulos
Ese colapso explica por qué las revisiones de código de un proyecto monolítico acaban discutiendo siempre las mismas cosas. No hay nada estructural que impida el acceso indebido, así que el único filtro disponible es la atención de una persona leyendo un diff, y ese filtro tiene una tasa de fallo que crece con el tamaño del cambio y con la hora del día. Un módulo traslada ese trabajo a un mecanismo que no se cansa, no negocia y no tiene una fecha de entrega encima.
Lo que se recompila cuando tocas una línea
El segundo retorno es el que más se nota en el día a día y el peor entendido. El compilador de Swift no recompila archivos: razona sobre dependencias. Dentro de un mismo módulo, la comprobación de tipos es global al módulo, porque cualquier declaración puede referirse a cualquier otra sin import alguno. El resultado es que un cambio en la firma de un tipo obliga a revisar un conjunto amplio de archivos, y en la práctica, en proyectos grandes, ese conjunto tiende a parecerse sospechosamente al total.
Merece la pena precisar por qué ese conjunto crece tanto. La compilación de Swift dentro de un módulo funciona en modo incremental por archivo, pero el compilador debe conocer las declaraciones de todos los demás archivos para resolver nombres y sobrecargas, y para ello construye una relación de dependencia entre archivos que es conservadora por diseño: ante la duda, invalida. Añade a eso las extensiones repartidas por el proyecto, la inferencia de tipos que atraviesa ficheros y el hecho de que en un target monolítico cualquier archivo es candidato a declarar cualquier cosa, y obtienes un grafo de invalidación denso donde tocar un tipo central arrastra a cientos de archivos que nunca lo mencionan directamente.
Con módulos separados el cálculo cambia de naturaleza. Cuando editas el cuerpo de una función public en FeaturePerfil, los módulos que dependen de él no necesitan recompilarse: su contrato no ha cambiado. Solo se recompila FeaturePerfil y se vuelve a enlazar. La frontera del módulo actúa como un cortafuegos de invalidación, y su eficacia depende de una sola cosa: que hayas cambiado la implementación y no la interfaz.
flowchart TB
edicion[Editas el cuerpo de una funcion en FeaturePerfil] --> pregunta{Cambio la firma publica}
pregunta -->|No| solo[Recompila solo FeaturePerfil y enlaza]
pregunta -->|Si| cascada[Recompila FeaturePerfil y todo lo que depende de el]
solo --> rapido[Segundos]
cascada --> lento[Minutos]
style solo fill:#a6e3a1,color:#11111b
style cascada fill:#f38ba8,color:#11111b
style pregunta fill:#cba6f7,color:#11111bEsto tiene una consecuencia que conviene interiorizar antes de partir nada: modularizar no acelera la compilación limpia. Al contrario, suele empeorarla ligeramente, porque hay más unidades que orquestar, más módulos que emitir y más enlazado. Lo que mejora, a menudo de forma espectacular, es la compilación incremental, que es la que haces cientos de veces al día. Confundir ambas métricas es el error más común al evaluar el resultado, y produce la conclusión exactamente contraria a la verdad.
Hay además una tercera métrica, invisible y decisiva: el tiempo de comprobación de tipos de expresiones complejas. SwiftUI genera cuerpos de vista con genéricos profundamente anidados, y el tiempo que el comprobador de tipos dedica a inferirlos crece de forma muy poco lineal. Aislar la interfaz en módulos pequeños acota ese trabajo a superficies manejables.
Conviene matizar el cortafuegos, porque no es absoluto y quien lo idealiza acaba decepcionado. Marcar una función como @inlinable publica su cuerpo en la interfaz del módulo para que los consumidores puedan integrarla; a cambio, cualquier cambio en ese cuerpo vuelve a invalidarlos. Lo mismo ocurre con @frozen sobre un enum o un struct público. Son herramientas legítimas de rendimiento en bibliotecas con estabilidad de ABI, y son casi siempre una mala idea dentro de una app propia, donde el enlazado es estático y el beneficio marginal no compensa perder la barrera de recompilación que acabas de construir.
Previews que arrancan en segundos
El tercer retorno es el que convierte la modularización de una mejora agradable en una necesidad. Un preview de SwiftUI no es un dibujo estático: es tu código compilado, enlazado y ejecutado dentro de un proceso que Xcode arranca. Ese proceso necesita el módulo que contiene la vista y todo lo que ese módulo arrastra consigo.
En una app monolítica, la vista más humilde arrastra literalmente la aplicación entera: el gestor de sesión, la capa de analítica, el cliente de red, el contenedor de persistencia, los ochenta ficheros de la pantalla de ajustes. Nada de eso interviene en dibujar el botón, pero todo tiene que estar compilado y enlazado para que el proceso arranque. De ahí los tiempos de espera de uno o dos minutos, y de ahí el hábito silencioso que se instala en todos los equipos que pasan por esto: dejar de usar preview y volver a lanzar la app en el simulador, navegando cinco pantallas cada vez para ver un cambio de color.
// En un modulo pequeno, esto compila y arranca en segundos porque
// el proceso solo carga FeaturePerfil y sus dependencias declaradas.
#Preview("Perfil con datos") {
PerfilView(usuario: .ejemplo)
}
Hay un matiz sobre el preview que casi nadie enuncia y que decide si el beneficio llega o se evapora: el proceso carga el módulo de la vista y todas sus dependencias declaradas. Si tu feature depende del módulo Comun y ese módulo depende a su vez de la red, la persistencia y la analítica, la has metido en el mismo agujero del que salías, solo que ahora con manifiestos. El arranque rápido no lo produce el hecho de tener módulos; lo produce tener módulos cuya clausura transitiva de dependencias sea pequeña. Es la primera razón práctica para vigilar el grafo, y la lección siguiente la desarrolla.
El efecto sobre la calidad del producto es indirecto y grande. Una iteración de dos minutos permite comprobar el caso feliz y poco más; una de tres segundos permite comprobar además el texto largo, el idioma con palabras compuestas, el modo oscuro, el tamaño de letra máximo, la lista vacía y el mensaje de error. La velocidad del bucle no acelera un trabajo fijo: cambia qué trabajo se hace, porque determina qué comprobaciones son lo bastante baratas como para hacerlas siempre en vez de dejarlas para el control de calidad o para el usuario.
Incremental, no limpia
La modularización mejora la build que haces cien veces al día, no la que haces una vez por semana. Mide la correcta.
Reglas verificadas
Una dependencia no declarada no compila. La arquitectura deja de ser un documento y pasa a ser una propiedad del proyecto.
Superficie explícita
Marcar algo public es una decisión. En un módulo único, internal equivale a público y nadie decide nada.
Previews vivos
Un preview carga su módulo y sus dependencias. Módulo pequeño, arranque corto, iteración real.
Medir antes de partir
Ninguno de los tres retornos es automático, y todos se pueden desperdiciar. Antes de mover un solo archivo, fija una línea base con tres escenarios distintos y anótalos por separado, porque agregarlos destruye la información.
# 1. Limpia y completa: el peor caso, se degrada al modularizar
xcodebuild -scheme App -destination 'generic/platform=iOS' clean build
# 2. Incremental tras tocar el cuerpo de una vista: el caso que debe mejorar
touch Sources/FeaturePerfil/PerfilView.swift
xcodebuild -scheme App -destination 'generic/platform=iOS' build
# 3. Incremental tras cambiar una firma publica: el caso que revela ciclos
Los dos primeros escenarios son los que discuten los equipos; el tercero es el que resuelve la discusión. Cambiar una firma pública y medir qué se recompila te dice si tu grafo tiene la forma que crees: si tocar el dominio invalida el proyecto entero, hay demasiadas cosas dependiendo de él o demasiadas cosas dentro de él, y ninguna cantidad de módulos adicionales lo va a arreglar mientras esa relación se mantenga.
Añade además la medida que casi nadie toma y que más correlaciona con la frustración diaria:
# Funciones cuya comprobacion de tipos supera los 200 ms
xcodebuild -scheme App build \
OTHER_SWIFT_FLAGS="-Xfrontend -warn-long-function-bodies=200" \
| grep "warning:.*ms"
Una advertencia metodológica que invalida la mitad de las mediciones que circulan: repite cada escenario al menos tres veces y quédate con la mediana, no con la primera pasada. La caché de módulos, el estado del disco y los procesos de fondo introducen una varianza que fácilmente supera al efecto que intentas medir, y una sola medición antes y otra después sirve para justificar cualquier conclusión que ya trajeras de casa.
El criterio de decisión es sencillo de enunciar y difícil de aceptar: si el escenario 2 ya es rápido y tus preview arrancan en pocos segundos, no tienes el problema que la modularización resuelve y pagarás su coste sin recibir su beneficio. La modularización es una respuesta a una medida, no a una intuición estética ni a una charla de conferencia.
Y hay un orden razonable para gastar el esfuerzo cuando la medida sí justifica actuar. Extrae primero lo que no depende de nada y muchos consumen —el dominio, los tipos de valor, el sistema de diseño—, porque son extracciones baratas, sin ciclos y con efecto inmediato sobre el grafo. Extrae después la feature que más veces se recompila, que suele coincidir con la que más gente toca a la vez. Deja para el final, o para nunca, las partes entrelazadas cuyo reparto todavía no está claro: modularizar lo que aún no entiendes solo fija por escrito una confusión que hoy es reversible.
Hay una asimetría profunda entre las dos formas de sostener un diseño, y la modularización vive justo en esa grieta. Una es la vigilancia: escribes la regla en un documento, la explicas en la incorporación de cada persona nueva, la recuerdas en las revisiones de código. Su coste es recurrente, proporcional al tamaño del equipo y a su rotación, y su tasa de fallo nunca llega a cero porque depende de la atención humana, que es un recurso caro y variable. La otra es la verificación: codificas la regla en la estructura del proyecto y el compilador la aplica siempre, gratis, sin cansarse y sin excepciones diplomáticas un viernes a las siete. Lo interesante es que este patrón no es exclusivo de los módulos: es el mismo movimiento que hace Swift al preferir tipos opcionales a la disciplina de comprobar nulos, al preferir Sendable verificado por el compilador a la convención de «no compartas esto entre hilos», al preferir actor a la costumbre de acordarse del candado. Un lenguaje entero construido sobre la idea de que las garantías estáticas valen más que las buenas intenciones. Modularizar es aplicar esa misma filosofía a la escala del proyecto: dejar de confiar en que nadie importe la capa equivocada y hacer que importarla, sencillamente, no compile. El coste es real —hay que declarar dependencias, hay que decidir qué es público, hay que mantener manifiestos— pero es un coste que se paga una vez al diseñar y no cien veces al revisar. Cuando el diseño y el mecanismo de cumplimiento son la misma cosa, el diseño no puede erosionarse en silencio, y esa es la única propiedad que distingue una arquitectura viva de un diagrama que alguien dibujó en 2021 y que ya no describe nada.
Un módulo es una unidad de compilación con superficie pública declarada; una carpeta no es nada que el compilador pueda verificar. Modularizar compra tres cosas medibles: recompilación incremental acotada, dependencias verificadas por el compilador y preview que arrancan rápido porque cargan poco. La compilación limpia empeora ligeramente, así que hay que medir el escenario incremental por separado o se concluye lo contrario de lo que ocurre.
- Mide los tres escenarios de compilación en tu proyecto actual y anótalos; repite cada medida tres veces y quédate con la mediana.
- Cronometra el arranque de un
previewde tu vista más simple y enumera qué código carga realmente ese proceso que no interviene en dibujarla. - Activa
warn-long-function-bodiesy localiza las cinco funciones más caras de comprobar; comprueba cuántas son cuerpos de vista. - Busca en tu código tres accesos que cruzan capas y que hoy compilan solo porque todo comparte el mismo
target. - Escribe el criterio explícito por el que decidirías no modularizar tu proyecto, y compruébalo contra las medidas que acabas de tomar.