wandres.dev
TCA VS ALTERNATIVAS · el juicio honesto

El coste real: líneas, compilación, contratación y dependencia

Adoptar TCA tiene un precio y se puede facturar en cuatro partidas concretas: el código que hay que escribir de más, los segundos que el compilador se queda cada día, las semanas de formación por persona y el riesgo de que tu arquitectura viva dentro de un repositorio que no controlas. Esta lección pone números y órdenes de magnitud a cada partida, separa el coste que se paga una vez del que se paga siempre, distingue el riesgo de abandono —que es folclore— del riesgo de migración —que es real y recurrente—, y termina con las mitigaciones que de verdad mueven cada número.

⏱ 23 min

Las discusiones sobre arquitectura degeneran cuando los costes se mencionan como adjetivos. Se dice que TCA es verbosa, que compila lento, que es difícil de aprender y que ata a una librería de terceros, y las cuatro afirmaciones son ciertas y completamente inútiles porque ninguna trae una magnitud. Un coste sin número no se puede comparar con un beneficio, y sin comparación no hay decisión sino preferencia. Esta lección hace lo contrario: toma las cuatro partidas del precio, les asigna órdenes de magnitud defendibles, separa lo que se paga una vez de lo que se paga cada día, y señala en cada caso la palanca que realmente mueve el número. El objetivo no es que adoptes o rechaces, sino que sepas cuánto estás firmando.

🎯 Al terminar esta lección sabrás
  • Estimar el multiplicador real de líneas y descomponerlo en lógica, nomenclatura y código que dejas de escribir.
  • Localizar dónde se va el tiempo de compilación en un proyecto con macros y constructores de resultados genéricos.
  • Presupuestar la formación por persona y compararla con el coste recurrente de una arquitectura propia sin documentar.
  • Separar el riesgo de abandono del riesgo de migración y elegir mitigaciones proporcionadas.

Líneas de código: el multiplicador y lo que oculta

El multiplicador que se observa al reescribir una feature equivalente con el mismo rigor está en torno a una vez y media a dos veces y media, y crece cuanto más pequeña es la feature. Una pantalla trivial puede triplicarse; una pantalla con seis fuentes de datos, tres destinos de navegación y cancelación se queda cerca de la unidad, y en algunos casos encoge. Esa dependencia del tamaño es la primera pista de que contar líneas mide mal: lo que se dispara es el coste fijo, no el coste marginal.

La descomposición del exceso es siempre parecida, y conviene hacerla porque las tres partidas tienen valor muy distinto. La nomenclatura obligatoria —un caso de enum por evento, una rama por caso— es mecánica, se escribe sin pensar y el compilador la vigila. Las declaraciones de dependencia son código que en un MVVM bien hecho también existiría, en forma de protocolo, implementación viva y doble de prueba, de modo que atribuírselas entero a TCA es un error de contabilidad muy común. Y la tercera partida, las afirmaciones de los tests, no es un coste sino el producto: son las líneas por las que se paga todo lo demás.

Contra eso hay que anotar el código que se deja de escribir, que nadie cuenta porque no está: la propiedad que guarda una tarea, la bandera de cancelación, el weak self, la clase falsa escrita a mano para cada colaborador, el estado duplicado entre un modelo y su vista, y las comprobaciones defensivas de estados que el enum ya hace imposibles.

Partida Signo Naturaleza
Casos de acción y ramas más mecánica, vigilada por el compilador
Claves de dependencia más existiría igual en cualquier diseño testeable
Afirmaciones del TestStore más es el producto, no el precio
Bookkeeping de tareas y banderas menos desaparece por completo
Dobles de prueba escritos a mano menos los sustituyen los valores de prueba

Tiempo de compilación: dónde se va exactamente

Este coste es el más real de los cuatro porque se paga muchas veces al día. Tiene tres fuentes distintas y conviene no confundirlas, porque cada una se ataca de forma diferente.

La primera es la expansión de macros, que ocurre antes de la comprobación de tipos y crece con el número de declaraciones anotadas por objetivo. Un módulo con treinta reducers expande treinta veces. La segunda es la inferencia de tipos sobre el constructor de resultados del body: una cadena larga de operadores anidados produce un tipo genérico profundo cuya comprobación no es lineal. La tercera es la ordinaria de cualquier proyecto grande, el grafo denso dentro de un objetivo monolítico.

# Cazar las expresiones y funciones que se comen el presupuesto
swift build -Xswiftc -warn-long-expression-type-checking=150 \
            -Xswiftc -warn-long-function-bodies=150

# Ver el coste de expandir macros por separado
swift build -Xswiftc -stats-output-dir -Xswiftc /tmp/stats

La mitigación que más mueve el número es la del Nivel 26: una feature por módulo, la implementación viva fuera del camino de las features y la interfaz estable. Dentro de un reducer hay una regla contraintuitiva que ahorra mucho: un Reduce gigantesco con veinte casos es más caro de comprobar que tres reducers de siete casos compuestos, aunque el total de líneas sea idéntico, porque la comprobación de tipos de un cuerpo no crece de forma lineal con su tamaño.

var body: some ReducerOf<Self> {
  Scope(state: \.sesion, action: \.sesion) { Sesion() }
  nucleo
  efectosDeRed
  sincronizacion
}

// Cada trozo es un reducer almacenado con su tipo anotado y comprobado aparte
private var nucleo: some ReducerOf<Self> {
  Reduce { state, action in ... }
}

Ese reparto tiene además un efecto secundario que se agradece: los errores de compilación pasan a señalar el trozo culpable en vez de la primera línea del body, que es donde el compilador los deposita cuando la expresión entera falla. Anotar el tipo de las expresiones que los avisos señalan completa la mitigación y suele recuperar una fracción notable del presupuesto.

⚠️
Mide el escenario, no el proyecto

Decir que un proyecto compila en cuatro minutos no significa nada si no se dice qué se tocó. Fija tres escenarios y anótalos por separado: limpio del paquete, incremental tras editar el cuerpo de una feature e incremental tras cambiar la firma pública de un cliente. Modularizar empeora levemente el primero y mejora radicalmente el segundo, así que quien solo mide el primero concluye lo contrario de lo que está ocurriendo.

El coste humano: formar, contratar y rotar

Es la partida que más se subestima y también la que peor se argumenta, porque se compara el coste visible de aprender TCA con el coste invisible de aprender la arquitectura casera de una empresa, que se presume nulo y nunca lo es.

El presupuesto realista de formación para un ingeniero competente en Swift es de dos a cuatro semanas hasta escribir features sin ayuda y de dos a tres meses hasta manejar composición, rutas de casos y dependencias con soltura. Es un coste alto, se paga una vez por persona, y tiene la propiedad de ser transferible: lo aprendido sirve en cualquier proyecto que use la librería, cosa que no ocurre con las convenciones internas de una empresa.

La contratación es la cara desagradable. El conjunto de personas que ya conocen TCA es pequeño, de modo que la estrategia sensata es contratar por competencia en Swift y formar, nunca filtrar por la librería. Hay además un riesgo simétrico que casi nunca se menciona: alguien que solo ha trabajado con TCA puede tener una comprensión frágil de SwiftUI a secas, y eso también es deuda.

La rotación es donde la cuenta se da la vuelta. En un proyecto con arquitectura propia, cada persona nueva paga un coste de incorporación que no está documentado en ningún sitio y que se cobra en preguntas a los veteranos; ese coste es recurrente, no se amortiza nunca y crece con la entropía del código. En un proyecto con TCA, la incorporación tiene un tramo empinado que se paga con material público y después se aplana mucho, porque hay una sola manera de escribir una feature y las mil que hay en el repositorio se leen igual.

Un plan de formación que funciona y que se puede presupuestar de verdad tiene cuatro pasos y dura entre tres y cinco semanas: leer una feature existente del propio repositorio y explicarla en voz alta; escribir un reducer sin efectos con su test exhaustivo; añadir una dependencia nueva con sus tres valores; y por último componer un hijo dentro de un padre y enrutar una acción de delegación. Quien completa los cuatro pasos ya es productivo, y la parte cara de la curva —composición avanzada, rutas de casos, navegación como dato— se aprende después sobre trabajo real y sin bloquear a nadie.

Depender de una librería: qué riesgo es real

El riesgo que se enuncia —que el mantenedor abandone y te quedes colgado— es el menos importante. La librería tiene licencia permisiva, el código está en tu disco, se puede bifurcar y una arquitectura sin evolución sigue funcionando indefinidamente. El riesgo real es otro y es continuo: el impuesto de migración.

TCA ha tenido varias transiciones mayores con cambios mecánicos pero extensos, y cada una costó semanas de trabajo repartido en proyectos grandes. A eso se suma que la arquitectura no es un paquete sino una constelación —dependencias, navegación, rutas de casos, colecciones identificadas, compartición— cada una con su propio ritmo de versiones. Quedarse atrás tiene un coste que crece de forma no lineal, porque las migraciones se acumulan y las guías envejecen.

Riesgo Probabilidad Impacto Mitigación proporcionada
Abandono del proyecto muy baja medio licencia permisiva y bifurcación posible
Migración mayor recurrente alta medio fijar versión, migrar pronto, presupuestarlo
Fuga de la librería a todo el código alta si no se vigila alto dominio en Swift plano, fronteras por módulo
Que Apple ofrezca lo mismo baja bajo ya ocurrió con observación y la mejoró

La mitigación que más protege no es técnica sino de diseño: mantener el dominio —reglas de negocio, cálculos, validaciones, tipos— en Swift plano sin ninguna importación de la librería, de modo que el reducer sea una capa fina que orquesta llamadas a código que sobreviviría a cualquier cambio de arquitectura. Ese consejo, además, mejora el código aunque nunca migres, porque obliga a separar lo que es negocio de lo que es coordinación.

// Objetivo de dominio: cero dependencias, sobrevive a cualquier migracion
.target(name: "Dominio"),

// Objetivo de feature: aqui y solo aqui entra la arquitectura
.target(name: "PedidoFeature", dependencies: [
  "Dominio",
  .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
]),

La segunda mitigación es de proceso y consiste en fijar la versión exacta en el manifiesto en lugar de aceptar el rango abierto, y programar la migración mayor como una tarea con presupuesto en cuanto sale, no cuando ya hay tres versiones de retraso. Una migración pendiente envejece mal: las guías se escriben para el salto inmediato, no para el acumulado, y quien se queda dos versiones atrás acaba haciendo el trabajo sin documentación. Un comando que conviene tener a mano es el que enumera qué versión resolvió realmente el paquete, porque el número que aparece en el manifiesto y el que se compila no siempre coinciden.

🧮

Cuenta las dos columnas

El multiplicador de líneas solo significa algo si también anotas el bookkeeping y los dobles que dejas de escribir.

⏱️

El bucle es el coste diario

Macros y genéricos profundos se pagan cada compilación. La frontera de módulo es la única palanca grande.

🎓

Formación transferible

Semanas por persona, una sola vez, con material público, y sirve fuera de tu empresa.

🪝

El riesgo es migrar

No temas el abandono. Presupuesta las migraciones mayores y mantén el dominio libre de importaciones.

flowchart TD
C[Coste total de adoptar] --> U[Se paga una vez]
C --> D[Se paga siempre]
U --> F[Formacion por persona]
U --> M[Migracion inicial del codigo]
D --> B[Bucle de compilacion]
D --> N[Nomenclatura por evento]
D --> V[Impuesto de versiones mayores]
B --> P[Palanca modularizar]
V --> Q[Palanca dominio en Swift plano]
style D fill:#f9e2af,color:#11111b
style P fill:#a6e3a1,color:#11111b
style Q fill:#a6e3a1,color:#11111b
Un coste que puedes ver y presupuestar no es peor que uno invisible: es infinitamente mejor, aunque la factura sea mayor

La razón por la que estas cuatro partidas producen tanto rechazo no es su tamaño sino su visibilidad, y ahí hay un sesgo que conviene desmontar porque distorsiona casi todas las decisiones de arquitectura que se toman en la industria. Cuando adoptas TCA, el precio aparece de golpe y con nombre: estas semanas de formación, estos segundos de compilación, estas líneas de más, esta migración cada tanto. Todo el mundo lo ve, todo el mundo lo puede citar, y por eso todo el mundo lo discute. Cuando en cambio construyes una arquitectura propia sobre convenciones, el precio no desaparece: se disuelve. Se paga en las tres horas que alguien dedica a entender por qué esta pantalla no se refresca, en la reunión donde se decide dónde va la lógica de este caso concreto, en el fallo que llegó a producción porque dos objetos mutaban el mismo campo, en la persona nueva que tarda un mes en atreverse a tocar el módulo de pagos, y en el test que nadie escribió porque montar el escenario era demasiado trabajo. Ninguna de esas partidas aparece en un presupuesto, ninguna tiene un responsable, y por eso se sienten como si fueran gratis. No lo son; son, con mucha frecuencia, más caras que la factura visible que tanto se debate. Esto no es un argumento a favor de adoptar: es un argumento a favor de comparar bien. Puede perfectamente ocurrir que en tu caso concreto la factura visible sea mayor que la invisible, y entonces la decisión correcta es no adoptar, con la conciencia tranquila de quien hizo la cuenta. Lo que no es defendible es rechazar por el tamaño de la factura visible sin haber intentado estimar la otra, porque eso no es prudencia sino preferencia por lo que no se mide. Y hay un corolario que vale para cualquier decisión técnica de este calibre: los costes que se pueden nombrar se pueden atacar, presupuestar y reducir con palancas concretas, mientras que los que se disuelven en el día a día solo se pueden sufrir. Elegir el coste visible es, antes que nada, elegir tener algo sobre lo que actuar.

⚔️ Levanta tu propia factura, con números tuyos
  1. Reescribe una feature mediana en TCA y cuenta líneas en las dos versiones, separando lógica, nomenclatura y afirmaciones de test. Anota también lo que dejaste de escribir.
  2. Fija los tres escenarios de compilación y mide antes de tocar nada. Después aplica los avisos de comprobación lenta y vuelve a medir.
  3. Cronometra a una persona del equipo desde cero hasta su primera feature aceptada en revisión. Ese número es tu presupuesto de formación real, no el de un artículo.
  4. Revisa el historial del repositorio y cuenta cuántas horas costó la última migración mayor de cualquier dependencia. Usa ese dato para presupuestar la próxima de TCA.
  5. Busca en tu dominio cuántos archivos importan la librería sin necesitarla. Cada uno que puedas dejar en Swift plano baja tu riesgo de migración sin coste alguno.