Back home

Swift Concurrency Series 08 | Organización de código asincrónico en proyectos reales

Lo realmente difícil es si todo el enlace asincrónico puede permanecer claro durante mucho tiempo.

Cuando solo hay una o dos solicitudes asincrónicas en el proyecto, gran parte del código no se ve tan mal. Una verdadera cuenca hidrográfica suele ocurrir cuando se dan simultáneamente las siguientes situaciones:

  • La misma página tiene primera carga, actualización desplegable, reintento y cambio de filtro.
  • La caché local y las solicitudes remotas deben participar juntas
  • Al salir de la página, cancelar algunas tareas y conservar otras
  • Varios módulos comparten ciertos recursos simultáneos

En este momento, encontrará que la mayor dificultad del código asincrónico ya es:

Cómo organizar todo el enlace asincrónico para que no se vuelva más disperso y difícil de cambiar tal como está escrito.

En este artículo no quiero hablar de una única API, sino de los principios organizativos que valoro más en proyectos reales.

1. Lo primero que comparto es la responsabilidad.

Cuando el código asincrónico falla, la primera reacción suele ser “desmantelar la función”. Dividir funciones es ciertamente útil, pero si las responsabilidades ya están mezcladas, dividirlas generalmente implica dividir el desorden en varios archivos pequeños.

Me preocupa más separar las responsabilidades primero. Generalmente dividido en al menos tres capas:

1. Capa de página

La capa de página es responsable de:

  • Activar la intención del usuario
  • Mostrar el estado de la página
  • Responder a los cambios interactivos.

Sabe “qué se debe cargar ahora”, pero no debería ser responsable de “cómo orquestar todo el proceso asincrónico”.

2. Capa de estado/Modelo de vista

La capa estatal es responsable de:

  • Traducir la intención del usuario en tareas.
  • Decidir si las tareas deben paralelizarse, reemplazarse o cancelarse.
  • Administrar la semántica de la página, como carga, cargada, fallida, etc.
  • Determinar qué resultados aún son elegibles para volver a escribir la página.

Es el verdadero punto final de los procesos asincrónicos.

3. Capa de servicio

La capa de servicio es responsable de:

  • Ajustar la interfaz
  • leer caché
  • Combina múltiples fuentes de datos
  • Proporcionar capacidades de dominio.

No debería saber cómo se ve la página, ni debería introducir la semántica del estado de la interfaz de usuario.

Una gran cantidad de código asincrónico está en mal estado porque una vez que se mezclan estas tres capas, cualquier cambio en los requisitos afectará la interfaz de usuario, el proceso, el estado y la fuente de datos al mismo tiempo.

2. El principio más importante de la capa de página: saber menos sobre los detalles asincrónicos

No me gusta que la página realice demasiados pasos asincrónicos por sí sola. Porque una vez que la página sepa demasiado, comenzará a abordar estas cosas:

  • Solicitar control de secuencia
  • Errores
  • Filtrar resultados
  • Política de cancelación
  • cargar la semántica de subdivisión

Al cambiar un requisito como este, la interfaz de usuario y el proceso suelen verse afectados juntos.

Entonces prefiero que la página exprese solo estas cosas:

  • “Necesito refrescarme ahora”
  • “El usuario hizo clic para volver a intentarlo”
  • “Las condiciones del filtro cambiaron”

En cuanto a lo que hay detrás de esto:

  • ¿Leer primero el caché o abrir primero la interfaz?
  • ¿Quieres dejar las viejas tareas?
  • Si el resultado ha caducado
  • ¿La primera carga y actualización deberían compartir el mismo enlace?

Ponga tanto como sea posible en el cierre de la capa de estado.

3. El estado de la página debe ser explícito, no confíe en un montón de fragmentos dispersos para deletrear la semántica.

Muchas páginas asincrónicas tendrán este aspecto en una etapa posterior:

  • isLoading
  • isRefreshing
  • hasError
  • showRetry
  • isEmpty
  • items

Estos valores son razonables cuando se ven individualmente, pero son propensos a contradecirse cuando se combinan:

  • Cargando con errores antiguos.
  • Ambos muestran el estado vacío y conservan la lista anterior.
  • Actualizado, pero el primer logotipo de carga todavía está ahí.

Por eso valoro el “estado semántico de la página” más que “muchos estados pequeños que se pueden combinar libremente”.

Porque lo realmente importante de una página asincrónica es si puede decir claramente en qué etapa se encuentra actualmente la página.

4. Los límites de las tareas deben ser claros; de lo contrario, todo se “ejecutará primero”

Si la estructura asincrónica es estable o no depende de si puede responder estas preguntas:

  • ¿A quién pertenece esta misión? -¿Continúa cuando se sale de la página?
  • Cuando llega una nueva tarea, ¿la anterior dejará de ser válida inmediatamente?
  • ¿Es esta una tarea independiente o parte de un proceso más largo?

Si estas preguntas no se pueden responder claramente, el siguiente código definitivamente entrará en un estado:

  • En todas partes parece que se explica por sí mismo.
  • Pero nadie puede hacer un recuento completo de cómo funciona este vínculo.

Si un fragmento de código asincrónico es difícil de repetir en lenguaje natural, generalmente será difícil mantenerlo estable más adelante.

5. Haré todo lo posible para incorporar la “validez de los resultados” al proceso.

Muchas páginas están desordenadas. A primera vista, parece que el resultado ha fallado, pero en realidad está más cerca:

  • Los resultados anteriores se devolvieron con éxito.
  • Pero ya no corresponde al contexto de la página actual.

Este tipo de problema es especialmente probable que ocurra cuando:

  • Buscar
  • interruptor de filtro
  • Paginación
  • Accede rápidamente a la página de salida.

Si la efectividad de los resultados no está incluida en el diseño, la página tarde o temprano desarrollará estos extraños fenómenos:

  • Reversión de contenido
  • la carga termina inexplicablemente
  • Los mensajes de error sobrescriben el estado de éxito actual

Entonces me importa:

  • ¿Quién juzgará si el resultado ha caducado?
  • Si cada punto de llamada debe ser juzgado por usted mismo
  • Aún cierra la interfaz uniformemente en la capa de estado.

Mis preferencias son muy claras: ** Intente ser coherente al cerrar y no permita que cada rama de vista juzgue por sí misma “si el resultado esta vez todavía cuenta”. **

6. No introduzca la semántica de la página en la capa de servicio

Hay mucho código en mal estado. En la superficie, parece que la capa de servicio no realizará solicitudes. De hecho, está más cerca de la capa de servicio y comienza a incorporar lentamente conceptos de página.

Por ejemplo, nombres o lógica como esta comienzan a aparecer en la capa de servicio:

  • “Solicitud especial para la primera carga de la página de inicio”
  • “La página de detalles está vacía y llena de lógica”
  • “Redacción incorrecta para esta página”

Una vez que se mezclen, las estructuras posteriores serán cada vez más difíciles de reutilizar. Debido a que la capa de servicio comienza a saber cómo se ve la página y la capa de página comienza a saber qué detalles de implementación tiene la capa de servicio, los límites se difuminan rápidamente.

La capa de servicio es más adecuada para centrarse en:

-¿Qué datos obtuviste?

  • Cómo combinar fuentes de datos
  • ¿Cuál es la estrategia de almacenamiento en caché?

En lugar de “¿cómo debería comportarse esta página?”

7. Me importa un principio: el proceso asincrónico debe recitarse claramente

Este es un criterio comúnmente utilizado cuando yo mismo reviso el código.

Si recoge un fragmento de código asincrónico, ya es difícil repetirlo en lenguaje natural:

  • ¿Cuál es la entrada?
  • ¿Cuál es el proceso principal?
  • ¿Qué tareas se cancelarán?
  • Qué resultados serán descartados
  • ¿Qué estados cambian según qué capa?

Incluso si este código se puede ejecutar hoy, lo más probable es que sea cada vez más difícil evolucionar en el futuro.

El verdadero temor del código asincrónico es que no está claro. Una vez que no puedas saberlo con claridad, cualquier iteración posterior es sólo cuestión de suerte.

8. Una idea organizacional que está más cerca de implementarse

Si quisiera resumirlo en una frase más práctica, lo organizaría así:

  • Capa de página: expresar intención
  • Capa de estado: tareas de cierre y semántica de estado.
  • Capa de servicio: proporcionando capacidades
  • Capa de recursos compartidos: use Actor u otros medios de aislamiento para administrar el estado compartido cuando sea necesario

Luego aclare estas relaciones en la capa de estado:

  • ¿Qué tareas son mutuamente excluyentes?
  • ¿Qué tareas son paralelas?
  • ¿Qué resultados se pueden descartar?
  • Qué estados debe ser actualizado por el actor principal.

Una situación común es que esto “parece una capa adicional”, pero en proyectos verdaderamente complejos, estas capas sirven para evitar que la lógica asincrónica se propague directamente a cada evento de página.

9. Conclusión: el núcleo de la organización asincrónica en proyectos reales es el límite

Lo más importante en un proyecto real nunca es si será Task, async let o Actor. Lo que realmente determina si el código puede evolucionar a largo plazo es si estos límites son claros:

  • Límite entre la capa de página y la capa de proceso
  • Límite entre la capa estatal y la capa de servicio
  • Límite entre el estado compartido y el estado normal
  • El límite entre los resultados válidos actuales y los resultados caducados.

Entonces, si tuviera que resumir este artículo en una frase, diría:

La clave para la organización del código asincrónico en proyectos reales no es “cómo escribir una API asincrónica”, sino “si los cuatro límites de tareas, estado, resultados y responsabilidades están claramente trazados”.

Sólo cuando estos límites sean claros podrá el código asincrónico realmente cambiar de “puede ejecutarse” a “puede mantenerse durante mucho tiempo”.