Arquitectura de interfaces web: Parte 1

Arquitectura, TDD, entorno de desarrollo y herramientas

“A child playing with a Jenga block tower” by Michał Parzuchowski on Unsplash

Este es el primer post de una serie donde quiero tratar de ilustrar, a través de ejemplos de código, una serie de conceptos de arquitectura de interfaces web y diseño orientado a tests.

⚠️ Esta serie no pretende ser una referencia académica, ni tampoco discutiremos los grandes patrones del mundo de OOP (Object Oriented Programming — Programación Orientada a Objetos) en relación a este tema, como MVC, MVP, ... En este caso vamos a hablar de grandes responsabilidades y abstracciones comunes a toda interfaz de forma genérica, iremos eligiendo convenciones inspiradas en el paradigma funcional, e implementaremos una arquitectura paso a paso hasta llegar a algo superficialmente parecido a React + Redux, pero íntegramente usando VanillaJS.

Arquitectura y TDD

La idea original de esta serie era hablar únicamente de diseño orientado a tests (TDD), pero en el mundo real, para poder testear algo, primero necesitamos ese algo que testear, una unidad aislada, e inevitablemente necesitamos hablar de ciertas abstracciones y responsabilidades en nuestra aplicación, una arquitectura. Esta arquitectura es la que nos da la primera capa de separación y unidades lógicas que ir construyendo (e ir testeando).

Usemos una analogía; imagina que tienes que construir una casa. Si empezáramos a construir directamente, sin diseñar primero un plano, probablemente nos encontraríamos con problemas para que las diferentes partes encajen y tengan sentido en conjunto. De la misma forma, antes de empezar a construir una interfaz, tiene sentido pararse a pensar en esa arquitectura (el plano), dibujar en papel y decidir un diseño general que nos permita comunicar el conjunto y la relación entra las partes.

Antes de ponerse a construir una interfaz web es necesario pensar en su arquitectura, que no es tanto el “qué” vamos a construir sino el “cómo”.

La arquitectura es un ejercicio de diseño, y las pruebas unitarias son una herramienta de diseño (valga la redundancia). De hecho la segunda D en TDD significa Design (Diseño).

Las pruebas unitarias son una herramienta de diseño, no de control de calidad.

Es por esto que en esta serie mezclamos arquitectura y TDD de forma intencional.

Pruebas unitarias

El término unitarias en pruebas unitarias hace referencia a que aquello que queremos probar son unidades aisladas. No se trata de la aplicación en general, ni tampoco historias de usuario — para eso existen otro tipo de pruebas: pruebas end-to-end, pruebase de integración, pruebas de carga, pruebas de usabilidad... recordemos que en este caso estamos hablando de pruebas unitarias.

Como principio de diseño, vamos a tratar de separar nuestro código en pedacitos (unidades), cada uno con una responsabilidad clara y minimizando las dependencias externas. Esto se alinea perfectamente al concepto de función pura y por tanto vamos a aspirar a implementar nuestro código como funciones puras. Si no estás familiarizada con el concepto te recomiendo leer este otro post sobre ese tema en particular:


La aplicación de ejemplo

En esta serie iremos construyendo gradualmente una aplicación web, introduciendo complejidad paso a paso y siempre acompañando nuestra implementación con tests como parte del proceso de diseño.

A lo largo de la serie hablaremos de Vistas (Views), Componentes (Components), Render, manejo de estado (State / Store), de HOCs (Higher-order Components) y otras abstracciones comunes en la arquitectura de interfaces, tomando prestadas nombres de funciones y responsabilidades de librerías populares (React y Redux principalmente).

Llegó la hora de diseñar nuestra aplicación de ejemplo. Empecemos en papel…

Storyboard de nuestra aplicación de ejemplo

Nuestra aplicación es muy sencilla (🙈), pero servirá como ejemplo mínimo viable. Nuestro storyboard muestra la siguiente historia de uso (desde el punto de vista de la interfaz):

  1. La primera pantalla presenta un botón de Sign In (inicio de sesión) al usuario.
  2. El usuario hace click en el botón.
  3. El botón aparece temporalmente como deshabilitado (disabled) y asumimos que se hace alguna consulta a un servidor.
  4. Recibimos una respuesta del servidor
  5. Mostramos una nueva pantalla con un mensaje de bienvenida

Esto servirá como punto de partida y primer documento de diseño de nuestra aplicación. En las siguientes partes de esta serie iremos identificando las distintas piezas que nos permitan solucionar el caso de uso, pero antes de empezar el viaje de abstracción e ir dividiendo nuestra aplicación en unidades con responsabilidades claras, primero preparemos nuestro entornos de desarrollo para tener las herramientas necesarias a la mano.


Entorno de desarrollo y herramientas

Continuando con la analogía de la construcción de una casa, una de las primeras decisiones que tenemos que afrontar como arquitectos es seleccionar una serie de herramientas y materiales para construir. En el mundo del software esto podría traducirse al entorno de desarrollo, lenguaje de implementación, librerías, frameworks, herramientas de productividad…

En el caso concreto de interfaces web, estamos restringidos a trabajar con las tecnologías que nos ofrece el navegador (HTML, CSS, JavaScript, DOM). Dentro de este contexto tenemos todavía mucha libertad para elegir herramientas, flujos de trabajo, arquitectura, … Para nuestra aplicación de ejemplo vamos a elegir una serie de herramientas (Jest, Webpack, Babel, …) para poder testear nuestro código, usar módulos (ES Modules) y así separar nuestro código en diferentes archivos entre otras cosas.

Antes de empezar con los temas de arquitectura, tomemos unos minutos para preparar el entorno de trabajo que nos permita ir construyendo la aplicación de ejemplo mientras avanzamos con las lecturas. Así podrás ejecutar el código, editarlo, poner console.log donde necesites, …

Empecemos por crear una carpeta con el nombre arch, donde vamos a construir nuestra aplicación. Entra en la nueva carpeta y crea las subcarpetas dist, src y test.

mkdir arch
cd arch
mkdir dist src test

En este ejemplo organizaremos nuestro código fuente (JavaScript) en la carpeta src, nuestros tests (pruebas) en la carpeta test (donde iremos replicando la misma estructura que vayamos creando en src, pero con los tests) y la carpeta dist contendrá los archivos de distribución (los archivos ya procesados que vamos a servir a través de un servidor web). Hay muchas maneras de organizar nuestros archivos a este nivel, lo importante es que tú y tu equipo se pongan de acuerdo en una convención clara y la respeten.

Ahora inicialicemos nuestro proyecto JavaScript. En este ejemplo vamos a usar yarn, pero podríamos usar también npm si así lo prefieres.

yarn init

Esto debería haber creado nuestro package.json, donde podemos ver la configuración de nuestra aplicación como un paquete de npm.

Eslint

Para nuestra aplicación de ejemplo hemos elegido usar Eslint con la configuración de airbnb-base. Es una muy buena práctica elegir una guía de estilo con tu equipo antes de comenzar a programar.

Instalemos Eslint, la configuración de airbnb-base y sus dependencias (eslint-plugin-import) como dependencias de desarrollo en nuestro proyecto:

yarn add -D eslint eslint-config-airbnb-base eslint-plugin-import

Una vez instaladas las dependencias, creemos una archivo de configuración para Eslint (.eslintrc) con el siguiente texto:

{
"env": {
"browser": true
},
"parserOptions": {
"ecmaVersion": 2018
},
"extends": "airbnb-base"
}

Nuestra configuración le indica a Eslint que vamos a trabajar en un entorno de navegador (nuestro proyecto es una interfaz web), queremos usar el parser de ES2018 y queremos heredar la configuración de airbnb-base.

Jest

Para ejecutar nuestros tests, hacer aserciones, mocks, espías, y coverage usaremos el maravilloso Jest. Así que lo instalamos como dependencia de desarrollo en nuestro proyecto:

yarn add -D jest

Babel

También usaremos Babel para poder hacer uso de ES Modules (import/export) junto con Webpack, así como otras bondades de ES6, ES7 y ES8.

yarn add -D babel-core babel-preset-env

Igual que hicimos con Eslint, Babel también necesita un archivo de configuración para saber qué preset queremos usar y qué plugins. Para esto creamos una archivo con el nombre .babelrc:

{
"presets": [["env", { "modules": false }]],
"env": {
"test": {
"plugins": [
"transform-es2015-modules-commonjs"
]
}
}
}

Nuestra configuración de Babel hace uso del preset env, e incluye un plugin para transformar ES Modules a CommonJS (para poder usar import/export en nuestros tests).

Webpack + Webpack dev server

Usaremos Webpack para construir nuestra aplicación y así transpilar, pre-procesar, minificar, … nuestro código fuente. Hemos elegido usar los directorios src y dist que son las rutas que Webpack espera por defecto (esto nos permite evitar crear un archivo de configuración de Webpack por ahora).

También usaremos Webpack dev server para servir nuestra aplicación localmente en modo de desarrollo. Esto nos permitirá ver nuestra aplicación mientras desarrollamos incluyendo hot reloading (recarga la aplicación automáticamente cada vez que salvamos cambios en el código fuente).

yarn add -D webpack webpack-cli webpack-dev-server

HTML

No podemos olvidarnos del HTML. En este ejemplo, vamos a crear un archivo index.html directamente en la carpeta dist. Este archivo será servido a través de un servidor web y representa el punto de entrada a la interfaz a través de un navegador web.

Sin más preámbulos, creemos el archivo dist/index.html:

<!doctype>
<html>
<head>
<meta charset="utf-8">
<title>Arch</title>
</head>
<body>
<div id="root"></div>
<script src="main.js"></script>
</body>
</html>

Como vemos, el cuerpo de nuestro HTML contiene solo lo siguiente:

  • un <div> con id root donde pintaremos nuestra interfaz.
  • un <script> que carga un archivo llamado main.js. Este archivo todavía no existe, pero será creado automáticamente cuando ejecutemos Webpack.

npm-scripts

Finalmente, tomémonos un momento para configurar algunos npm-scripts que nos ayuden a ejecutar las tareas del ciclo de desarrollo:

...
"scripts": {
"pretest": "eslint src test",
"test": "jest --coverage",
"build": "webpack --mode=production",
"start": "webpack-dev-server --content-base dist/ --mode=development"
},
...
  • pretest se invoca automáticamente antes que la tarea test y en nuestro caso invoca nuestro linter (eslint).
  • test se invoca cuando ejecutamos el comando yarn test (o npm test) y lo hemos configurado para que ejecute nuestros tests (con coverage).
  • build se invoca cuando ejecutamos yarn build o npm run build y lo usamos para procesar el código fuente de nuestra aplicación y producir el archivo dist/main.js usando Webpack.
  • start se invoca cuando ejecutamos yarn start o npm start y en este caso usa webpack-dev-server para servir nuestra aplicación localmente en modo de desarrollo.

Llegado a este punto deberías de tener la siguiente estructura de archivos en la carpeta de tu proyecto (arch):

.
├── dist
│ └── index.html
├── package.json
├── src
├── test
└── yarn.lock

Ya estamos listos para empezar a diseñar nuestra aplicación! 🚀 👷


Este post es parte de la serie “Arquitectura de interfaces web”. Si quieres continuar leyendo…

Parte 2: Vistas, componentes y renderizado >>


Material complementario

Aprovecho a dejar acá links a los slides y video del taller presencial que se dió en Laboratoria Lima el 13 de Julio de 2018.

Slides

Video