Estructurando aplicaciones web
Como dije en la página anterior, todos los módulos deben construirse en torno a un tipo central. Por lo tanto, si estuviera construyendo una aplicación web para artículos de un blog, comenzaría con módulos como estos:
Main
Page.Home
Page.Search
Page.Author
Tendría un módulo para cada página, creados en torno al tipo Model
. Estos módulos siguen la Arquitectura Elm con los típicos Model
, init
, update
, view
y cualquier otra funcion auxiliar que haga falta. De ahí en adelante simplemente dejaría crecer estos módulos, añadiendo los tipos y funciones que sean necesarios. Si llego a notar que creé un tipo personalizado junto a un par de funciones auxiliares, puede que los mueva a un módulo propio.
Antes de entrar a ver algunos ejemplos, quiero enfatizar una estrategia importante.
No planifiques por adelantado
Nota que mis módulos Page
no hacen suposiciones sobre el futuro. No traté de definir módulos que puedan ser usados en múltiples lugares. No traté de compartir funciones. Esto es muy a propósito.
Al principio en cada uno de mis proyectos, siempre tengo un gran plan de cómo va a funcionar todo en conjunto. “Las páginas para editar y ver artículos ambas tienen que ver con artículos, así que voy a escribir un módulo Post
compartido”. Pero mientras voy escribiendo la aplicación, noto que sólo la página de visualización de artículos necesita una fecha de publicación. Y realmente necesito manejar la edición de forma especial para cachear los datos al cerrar la pestaña. Y por lo mismo, los datos también necesitan guardarse de manera distinta en el servidor. Etcétera. Termino haciendo que el módulo Post
se convierta en una confusa maraña que se encarga de todos estos casos particulares, y se hace más incómodo su uso en ambas páginas.
Al sólo empezar con las páginas, se hace mucho más fácil ver cuándo las cosas son similares, pero no lo mismo. O sea, ¡lo más típico en interfaces de usuario! En el editar y ver artículos, es muy probable que terminemos con un tipo EditablePost
y otro ViewablePost
, cada uno con una estructura distinta y sus propios funciones auxiliares y decodificadores de JSON. Tal vez esos tipos son suficientemente complejos como para que tengan su propio módulo. O tal vez no. Tendríamos que escribir el código y ver qué pasa.
Esto es posible porque el compilador hace que sea muy fácil hacer grandes refactorizaciones. Si de pronto me doy cuenta de que cometí un gran error que compromete 20 archivos, puedo simplemente ir y corregirlo.
Ejemplos
Estos proyectos de código abierto funcionan como ejemplos de la estructura que acabo de mencionar:
Choque cultural
La gente que viene de JavaScript tiende a traer sus hábitos, expectativas y ansiedades específicas de JavaScript consigo. Son legítimamente importantes en ese contexto, pero pueden causar serios problemas al transferirlos a Elm.
Instintos defensivos
En “The Life of a File” hago hincapié en cierto conocimiento popular del mundo de JavaScript que se puede convertir en una trampa en Elm.
“Prefiere archivos cortos.”En JavaScript, mientras más largo sea tu archivo, más probable será que tengas una mutación escondida que causará un bug complicado. Pero en Elm, eso simplemente no es posible. Tu archivo podría tener hasta 2000 líneas y seguirá sin ser posible.“Parte con la arquitectura correcta.”En JavaScript, refactorizar es extremadamente riesgoso. En muchos casos es menos costoso simplemente reescribir todo desde cero. Pero en Elm, refactorizar implica poco costo y riesgo. Puedes cambiar 20 archivos sin preocupación.Estos instintos defensivos nos protegen de problemas que no existen en Elm. Pero entender esto intelectualmente no es lo mismo que entenderlo en forma instintiva, y he observado que gente que programa JS se siente profundamente incómoda cuando ve archivos que superan las 400, 600, u 800 líneas. Te invito a que superes tu propio límite de líneas. Ve hasta dónde puedes llevarlo. Usa comentarios como encabezado, crea funciones auxiliares, pero siempre déjalo todo en el mismo archivo. Me parece que tener personalmente esta experiencia es algo muy valioso.
MVC
Hay gente que ve la Arquitectura Elm y tiene la intuición de dividir su código en módulos distintos
Model
,Update
yView
. ¡No lo hagas!Esto conlleva a tener fronteras debatibles y poco claras. ¿Qué pasa cuando
Post.estimatedReadTime
se use tanto en las funcionesupdate
comoview
? Es algo muy razonable, pero no hay una evidente pertenencia a una u otra. ¿Tal vez necesitamos un móduloUtils
? ¿Tal vez en realidad es alguna especie de controlador? El código resultante tiende a ser difícil de navegar, porque la ubicación de cada función se convirtió en una pregunta ontológica, y tus colegas tendrán cada uno su propia teoría. ¿Qué es unestimatedReadTime
, realmente? ¿Cuál es su esencia? ¿La estimación? ¿Qué diría Ricardo que es su esencia? ¿El tiempo?Si construímos cada módulo en torno a un tipo, muy rara vez nos toparemos con este tipo de preguntas. Tenemos un módulo
Page.Home
que contiene suModel
,update
yview
. Escribimos funciones auxiliares. Añadimos un tipoPost
, eventualmente. Añadimos una funciónestimatedReadTime
. Tal vez un día hayan suficientes funciones auxiliares sobre el tipoPost
, y tal vez valga la pena separar ese código en su propio módulo. Usando esta convención acabamos gastando menos tiempo considerando y reconsiderando las fronteras entre módulos. En mi opinión, el código también queda mucho más claro.Componentes
Gente acostumbrada a React espera que todo sea componentes. Tratar intencionadamente de construir componentes es una fórmula para el desastre en Elm. El problema fundamental es que los componentes son objetos:
- componentes = estado local + métodos
- estado local + métodos = objetos
Sería extraño usar Elm y preguntarse, “¿cómo uso objetos para estructurar mi aplicación?”. ¡No hay objetos en Elm! Los miembros de la comunidad recomiendan usar tipos personalizados y funciones.
Pensar en términos de componentes fomenta que creemos módulos basados en el diseño visual de nuestra aplicación. “Hay una barra lateral, así que necesito un módulo
Sidebar
.” Sería mucho más fácil simplemente hacer una funciónviewSidebar
y pasarle los argumentos que necesita. Tal vez ni siquiera tiene estado. Tal vez necesita uno o dos campos. Pongámolos en elModel
que ya tenemos. Si realmente vale la pena separarlo en su propio módulo, lo sabremos porque tendremos un tipo personalizado con varias funciones auxiliares relevantes.El punto es que escribir una función
viewSidebar
no significa que necesitemos crear también unupdate
y otro tipoModel
junto con ella. Resiste el instinto. Escribe las funciones auxiliares que necesites y nada más.