Diseño de software

Escapa del infierno de los if con el patrón State de Swift

El patrón State agrupa el comportamiento y las transiciones de cada estado en objetos o enums, reduciendo las condiciones dispersas. Explica cómo refactorizar estados de pantalla en Swift y distinguir, por intención, las estructuras que parecen el patrón Strategy.

4 min de lectura
Imagen de portada de Escapa del infierno de los if con el patrón State de Swift

¿Alguna vez has tenido que abrir if y modificar diez sentencias if solo para cambiar un estado de pantalla?

Cuando todos los estados de carga, éxito y error se gestionan con if isLoading y else if hasError, el código se vuelve inmanejable enseguida.

Hoy explicaré, basándome en experiencia real, cómo organizar el infierno de los if en objetos de estado con el patrón State de Swift.

La idea principal es esta: el patrón State consiste en separar cada estado en un objeto independiente (o enum) y colocar la lógica de transición dentro de ese objeto. Así, las ramas condicionales quedan centralizadas.

La diferencia con el patrón Strategy, cuya estructura es casi idéntica, está en quién impulsa el cambio. Patrón Strategy vs. patrón State permite distinguir primero la selección externa de la transición interna de estados.

Qué aprenderás

  1. Por qué aparece el infierno de los if
  2. Dos formas de dividir estados en objetos en Swift
  3. Cuándo usar enum o protocol
  4. Un orden de refactorización aplicable en la práctica

¿Por qué aparece el infierno de los if?

Al principio, con solo un par de estados, if parece suficiente. Pero al crecer los requisitos, aumentan a 5 o 6: carga, éxito, error, pantalla vacía, reintento y demás.

El problema es que estas combinaciones de estados se dispersan entre varios métodos. Se repiten en la lógica de activación de botones como if, en la actualización de pantalla como if y en los callbacks de red como if. Por eso, al añadir un estado hay que localizar y modificar cada bloque if; basta olvidar uno para crear un bug.

Cuando los estados están dispersos, los bugs se esconden; cuando se agrupan, salen a la luz.

El patrón State evita precisamente esta «dispersión».


Cómo dividir estados en objetos en Swift (con enum)

La forma más ligera de empezar es enum. Puedes definir los estados como valores y almacenar los datos necesarios mediante valores asociados. Este es un ejemplo de estados de pantalla representados con un enum.

enum ViewState {
    case loading
    case loaded(items: [String]) // Incluir datos en caso de éxito
    case failed(message: String) // Incluir el motivo del error
    case empty
}

Ahora la actualización de pantalla termina con switch una sola vez. Los valores asociados impiden que exista un estado ambiguo como «éxito con datos nil».

switch state {
case .loading:      showSpinner()
case .loaded(let items): render(items)
case .failed(let msg):   showError(msg)
case .empty:        showEmptyView()
}

Para la mayoría de los estados de pantalla, el enfoque enum es suficiente. switch detecta los casos omitidos en compilación, así que añadir estados resulta más seguro.

Agrupar los estados de pantalla en un solo switch hace el código mucho más manejable
Agrupar los estados de pantalla en un solo switch hace el código mucho más manejable

enum vs. protocol: ¿cuándo usar cada uno?

Si el comportamiento de cada estado es muy distinto y las reglas de transición son complejas, conviene usar protocol. Cada estado se convierte en un tipo que devuelve por sí mismo el siguiente estado. Estas son las diferencias.

Categoría Enfoque enum Enfoque protocol
Situación adecuada Estados centrados en datos Cada estado tiene un comportamiento distinto
Añadir un estado Añadir un case Añadir un tipo
Lógica de transición switch externo Encapsulada dentro de los objetos de estado
Curva de aprendizaje Baja Algo alta

La clave del enfoque protocol es que cada objeto se responsabiliza de sus transiciones de estado.

protocol PlayerState {
    func play() -> PlayerState  // Devolver el siguiente estado
    func pause() -> PlayerState
}

struct PlayingState: PlayerState {
    func play() -> PlayerState { self }        // Mantenerlo si ya se está reproduciendo
    func pause() -> PlayerState { PausedState() } // Cambiar a pausa
}

Así, reglas como «¿qué ocurre si se pulsa pause mientras se reproduce?» existen únicamente dentro de ese estado. Desaparecen las ramas y cada estado solo conoce sus propias reglas.

Transición basada en protocol, donde cada estado decide el siguiente
Transición basada en protocol, donde cada estado decide el siguiente

Refactorización práctica: sigue este orden

Intentar cambiarlo todo de una vez puede intimidar. Yo hice la migración poco a poco y en este orden.

  1. Anotar la lista de estados que realmente representa el if disperso
  2. Definir esos estados en un único enum
  3. Sustituir primero el código de actualización de pantalla por switch
  4. Eliminar por completo con enum las combinaciones imposibles, como carga y error simultáneos
  5. Promover a protocol solo las partes con reglas de transición complejas

La clave es empezar con un enum y promoverlo a protocol solo cuando sea necesario. Diseñar todo con protocol desde el principio puede hacer que el código resulte más pesado.

Es mucho más fácil dibujar los estados así y después trasladarlos a un enum
Es mucho más fácil dibujar los estados así y después trasladarlos a un enum

Preguntas frecuentes (Q&A)

P. ¿Debo usar el patrón aunque solo haya 3 estados?

No es obligatorio. Sin embargo, si los estados están dispersos entre varios métodos, conviene agruparlos en un enum independientemente de cuántos haya.

P. ¿También se puede usar con SwiftUI?

Sí. Encaja muy bien si mantienes el estado como @Published var state: ViewState y haces la bifurcación en la vista mediante switch.

P. ¿No se vuelve desordenado un enum con muchos valores asociados?

Cuando superan los 3 valores asociados, recomiendo agruparlos en un struct independiente y almacenarlos como case loaded(Result).


Puede resultar extraño al principio, pero cuando aprendas a agrupar los estados en objetos no querrás volver al código anterior. Empieza organizando una pantalla pequeña con un enum. Saldrás del infierno de los if antes de lo que esperas. ¡Ánimo!

Seguir leyendo