O arquitectura de un framework web...
Lo primero que necesitamos, al hacer una aplicación web, o de cualquier otra clase, es...
Control de errores
Habrán errores - o tal vez debería decir: Excepciones. El sistema fallará. Queremos poder:
- Capturar las excepciones.
- Guardar la información de las excepciones.
- Comunicarlas al desarrollador.
Capturar las excepciones
La primera parte es relativamente sencilla. Casi toda plataforma tiene alguna forma de capturar errores. Si la plataforma que utilizas no lo tiene, considera crearlo o cambiar de plataforma (cambiar de plataforma es más fácil).
Guardar la información de las excepciones
Guardar la información es un poco más complicado... cualquier servidor que se respete tiene varios procesos para manejar las peticiones, y si agregamos la información de los errores en un log, necesitamos sincronizar el acceso al log. Este proceso puede fallar - y en verdad no queremos fallos en un sistema de control de errores. Además no todos los lenguajes tienen primitivas de sincronización. Podemos recurrir al sistema de archivos para hacer esto... pero solo tenemos dos operaciones con garantías de transacción atravesar de todas las plataformas: 1) Crear un archivo nuevo fallará si el archivo ya existe. y 2) Mover un archivo fallará si el archivo de destino ya existe.
En consecuencia, la forma segura de manejar los errores es crear un archivo nuevo para guardar la información del error. Necesitamos un esquema que minimice el riesgo de colisión. La siguiente información es útil: Fecha y hora, id del proceso, variables de desempeño, generar números pseudoaleatorios.
Es importante destacar que crear un archivo nuevo aun puede fallar, si se llena el volumen de almacenamiento (el disco esta lleno).
Comunicarlas al desarrollador
Lo ideal es mostrar esta información en la respuesta. Para esto necesitamos presentar la información en una forma que sea fácil de leer. Esto implica generar código HTML para presentar la descripción del error y la pila de llamadas... por lo menos.
Sin embargo, debemos hacer esto, si y solo si: El entorno no es producción (estamos en desarrollo o pruebas - y de pruebas no debería pasar si aun están saliendo errores) y la petición es local.
Sin embargo, ocurrirán errores en producción. En este caso, no las mostraremos, porque pueden contener información útil a usuarios mal intencionados que buscan hacer daño al sistema.
Pero, determinar si estamos en producción o no, no debe depender de un sistema de configuración... porque el sistema de configuración puede tener errores... esto significa que el sistema deberá poder determinar si se encuentra en producción, sin utilizar configuración. Para esto, mi solución es usar una carpeta. Es fácil verificar si una carpeta existe o no, y tiene garantías bien conocidas.
Si estoy desplegando por FTP o similar, puedo copiar todo y esto no eliminará carpetas del destino. Si estoy utilizando un sistema de control de versiones basado en archivos (como git), este solo creará carpetas si no están vacías. Por esto, opto por utilizar una carpeta para marcar un entorno de producción y no al contrario. Además, si fallo en marcar el entorno de producción, las peticiones que recibe el entorno de producción no serán locales, así que no debería mostrar los errores.
Pero ¿cómo comunico los errores en producción al desarrollador?Para esto necesitamos un segundo canal, comúnmente correo electrónico. El cual requiere configuración. Una falla de configuración implicará que no se comunica el error. Será responsabilidad del desarrollador verificar los errores de forma rutinaria, y no poner en producción configuración que no sirva.
Hasta ahora nos hemos dado cuenta que: necesitamos configuración y necesitamos enviar correo electrónico.
Configuración
La configuración no es igual en todos los entornos. El entorno de desarrollo se conectará a una base de datos diferentes que el entorno de prueba, y este a una diferente que a producción.
De esta forma, un accidente en pruebas - debido a un defecto aun no corregido - no dañará los datos de producción. Además cada desarrollador puede poner los datos que quiera en su entorno de desarrollo sin "pisar las mangueras" de los otros.
Así que, el sistema deberá identificar su entorno. Ya no me refiero a saber si está en producción o no. Puesto que es posible desplegar múltiples copias de la aplicación - por ejemplo para servir a diferentes regiones. Lo que necesitamos es un Id de entorno. Para esto, el nombre del usuario del sistema operativo y la ruta en que se encuentra guardado el sistema bastarán.
Ahora que tenemos el id del entorno, podemos buscar archivos de configuración específicos para este entorno. Aun así debemos notar que habrá configuración que no cambia de un entorno a otro. De hecho, hay configuración que cambia:
- Nunca. Es la misma en todo caso.
- De un entorno a otro.
- De un servidor a otro (pueden haber varios entornos en un mismo servidor).
- De un dominio a otro (pueden haber varios dominios en un servidor, pueden haber varios servidores en un dominio).
Nota 1: La configuración por dominio rara vez es útil. Lo más común es que podemos desplegar una aplicación diferente para cada dominio. Pero a veces el cliente quiere varias copias de la misma aplicación, con configuración diferente... y es mucho más fácil enrutar todo a una sola copia y que el sistema cargue configuración diferente dependiendo de donde viene.
Para distinguir el servidor, guardaremos un nombre para el servidor como configuración del entorno. Una vez leemos este nombre podemos leer la configuración del servidor que le corresponde. Para los dominios, como configuración de servidor se guardará una lista blanca. El sistema puede ver que dominio está pidiendo el cliente, verificar si está en la lista blanca. Si está, puede cargar la configuración por defecto del dominio (si existe). El sistema de control de acceso puede denegar el acceso basado en que falta configuración.
Cuando estamos desplegando una copia nueva, y la configuración del entorno nuevo aun no existe... o si algún archivo de configuración falta... es útil que el sistema pueda crear archivos de configuración vacíos con cada dato o con valores por defecto sensibles. Esto significa que el sistema de configuración puede escribir, no solo leer, es posible que no pueda escribir todos los formatos que puede leer.
Para darle valor por defecto al servidor, el sistema puede ver qué dominio base que está pidiendo el cliente. Este es un buen nombre por defecto para el servidor (puesto que es raro que tengamos que desplegar varios dominios). Si no es adecuado el desarrollador puede cambiarlo en el archivo de configuración. Pero definitivamente no tendrá que entrar en una base de datos relacional para hacer esto (Te estoy viendo Wordpress).
Nota 2: Algunos sistemas hacen balance de carga lanzando nuevas instancias de maquinas virtuales, estas pueden generar nuevos id de entorno. Si es así, debido a que hacen balance de carga para el mismo servidor, es necesario que el sistema pueda relacionar los nuevos id de entorno automáticamente con el servidor. Sin necesidad que un programador acceda y los modifique.
Nota 3: No, la configuración no va en la base de datos. Se necesita configuración para poder conectarse a la base de datos, por tanto la configuración debe poder funcionar sin base de datos (Te sigo viendo, Wordpress).
Nota 4: No es responsabilidad del sistema de configuración identificar el dominio, ni el id entorno. Esta información será provenida por servicios.
¿Qué formato utilizar para los archivos de configuración?
Todos los que sea posible. No es responsabilidad del sistema de configuración encontrar los archivos de configuración (ya explicaré porqué), entonces el sistema de configuración puede ver que formato extensión tiene el archivo de configuración, y procesarlo adecuadamente. Sea .json, .xml, .ini, etcétera.
Hasta ahora hemos identificado que: necesitamos enviar correo electrónico, un sistema para bases de datos, un sistemas para encontrar archivos, un sistema de control de acceso, un sistema para cargar servicios.
Correo Electrónico
Nada del otro mundo. Es cuestión de encontrar una librería que permita enviar correo electrónico, y pedir al sistema de configuración los datos necesarios.
Es útil tener un servicio que pueda extraer texto de HTML, de forma que podamos especificar el contenido HTML solamente.
Base de datos
Bienvenidos a la guerra. Hay quienes piensan que la base de datos es primero, y otros que los objetos son primero. Y definitivamente quiero un sistema que convierta lo uno en lo otro.
Para mi, en la actualidad, la base de datos va antes que los objetos. Sin embargo, no es primero. Primero es la configuración.
Puedo utilizar archivos de configuración para guardar información relevante para:
- Crear las tablas en la base de datos, independientemente del motor.
- Crear objetos que permitan acceder y manipular la base de datos.
- Crear representaciones visuales de los datos (reportes y formularios).
Si describimos la base de datos en archivos de configuración, el sistema puede instalar la base de datos, con solo una conexión valida. Esto soluciona el problema de sincronizar scripts de base datos en el sistema de control de versiones.
Queremos un sistema que permita conectarse a varios motores. Pero la conexión a los diferentes motores es diferente. Para esto creamos códigos adaptadores, de los cuales cargamos el adecuado según la configuración.
Hacer estas dos cosas resuelve automáticamente el problema de migrar la base de datos a otro motor. Es cuestión de configurar la conexión y tener el adaptador adecuado, y el sistema crea la base de datos.
En cuanto a los objetos para acceder y manipular la base de datos... depende de lo que nos permita hacer el lenguaje.
Para la representación visual, vamos a necesitar cosas que la base de datos no necesita, por ejemplo términos para cuando no hay elementos, cuando hay uno, y cuando hay varios; Descripciones de cada dato; El tipo de validación; Operaciones para hacer antes y después de consultar, insertar, actualizar y eliminar (y no me refiero a disparadores).
Lo que me recuerda, necesitamos un sistema para traducir la aplicación a otros lenguajes... Ah, espera, ese es el sistema de configuración, necesitamos cargar configuración basada en el lenguaje (que se guardaría como variables de sesión) y un servicio que tome una plantilla de texto (que tomamos de configuración) y un objeto. No una lista de parámetros, un objeto del que el servicio pueda leer propiedades... de esa forma no tenemos que saber los parámetros de antemano, permitiendo que sean diferentes en cada idioma.
Que no me refiero a disparadores... por ejemplo, si hay un archivo asociado a un registro en la base de datos, lo quiero eliminar cuando elimine el registro. O por ejemplo, puede que necesite campos calculados que no guardo en la base datos (por que son redundantes). O por ejemplo, antes de insertar debo agregar unos campos extra. Etcétera.
Hasta ahora hemos identificado que: necesitamos un sistemas para encontrar archivos, un sistema de control de acceso, un sistema para cargar servicios, un sistema para validar la entrada del usuario.
Encontrar archivos
El sistema no debe asumir en que ruta se encuentra. El desarrollador no debe tener que resolver rutar relativas mentalmente. Un sistema solo para esto, es casi obligatorio en todos los lenguajes dinámicos.
Pero este sistema es algo más, lo que queremos es poder encontrar archivos, dada un serie de carpetas. Intentará conseguir un archivo con el nombre solicitado (sin importar la extensión), si no lo encuentra, descarta una carpeta y busca en la que sigue.
De esta forma, el sistema de configuración puede pedir un archivo que está en la ruta de configuración común, del entorno, del servidor, o del dominio. Y este sistema encuentra el archivo de configuración adecuado...
Ah, pero la ruta de la configuración de servidor, depende de leer la configuración del entorno! Bien, el sistema para encontrar archivos, debe poder aceptar funciones en lugar de cadenas de texto, u otra forma de ejecución diferida.
Servicios
Los servicios pueden ser simplemente funciones estáticas. Pero a veces es útil que se carguen de forma dinámica, de hecho a veces es útil que sigan el mismo patrón que la configuración. Es por esto que encontrar archivos se separó de la configuración: para poder usarlo para encontrar servicios.
En generar un servicio es un fragmento de código... he aquí algunos usos:
- Recuperar información de la petición
- Generar identificadores únicos
- Leer configuración especifica para un archivo (a modo de meta datos del archivo)
- Leer datos que puede que vengan de configuración (recuerda que el sistema de configuración puede escribir, no solo leer, esto lo hace útil como cache), o puede que no.
- Pre-procesar archivos (por ejemplo, para web queremos JPG progresivos, o queremos minimizar y comprimir CSS y JavaScript, o queremos compilar LESS, TypeScript, etcétera)
- Verificar si el usuario tiene un permiso especifico (preguntando a control de acceso)
Hasta ahora hemos identificado que: necesitamos un sistema de control de acceso, un sistema para validar la entrada del usuario. Pero primero...
Enrutamiento
El cliente solicita una url. Es necesario encontrar un archivo que maneje esta url y genere una vista. Esto es, un presentador. Cada ruta o conjunto de rutas tendrá una respuesta distinta... estas cosas se deben considerar:
- Qué presentador manejará la petición
- Qué plantilla (ah, si, necesitamos plantillas, porque hay cosas que queremos mantener constantes en todo el sitio)
- En qué casos se debe presentar sin plantilla (por ejemplo, no usaremos plantillas en peticiones que no sean GET, y no las usaremos en Ajax)
- Qué código de estado HTTP devolver (por ejemplo: 404)
- Qué tipo de contenido (por ejemplo: text/html; charset=UTF-8)
- Si el cliente debe guardar la respuesta en cache
- Qué pre-procesadores necesitamos
Addendum: Es posible que el presentador decida cambiar estos datos. Muchos recurren a guardar el resultado del presentador en un buffer, y luego poner las cabeceras HTTP, y luego poner el contenido del buffer. No es necesario. Basta con guardar estos datos y mandarlos justo antes de que el presentador mande el primer bit. Así el presentador tiene oportunidad de cambiarlos antes de mandar el primer bit. Dependiendo la plataforma esto puede ser fácil o difícil de implementar. Nota: Esto es un caso extremo de todas formas. En general, control de acceso puede hacerlo.
Es necesario notar que un presentador puede ser código o contenido (CSS, JavaScript, etcétera) y los pre-procesadores funcionan sobre contenido.
Además es necesario hablar con el sistema de control de acceso, para verificar la autorización del usuario.
Y es necesario identificar que cosas vamos a poner en pre-carga, en particular si usamos HTTP/2.
Control de acceso
Tiene dos partes: autenticación y autorización.
La autenticación se encarga de manejar la contraseña de los usuarios. Necesita una conexión a la base de datos, una tabla en la base de datos, y los campos que va a utilizar, y los que va a devolver. Además necesita saber el algoritmo criptográfico que va a utilizar. Todo esto viene de configuración.
De hecho, no debe apuntar a una tabla, sino a la descripción de una tabla en configuración. Es posible que el sistema de autenticación deba saber de las relaciones entre tablas si es necesario devolver una lista de permisos. Por ejemplo: si en el sistema un usuario solo tiene un rol, basta con acceso a una tabla. Pero si el usuario tiene una lista de roles, significa que hay una relación muchos a muchos entre roles y usuarios, y para conseguir la lista necesitamos consultar varias tablas.
Autorización funciona con una lista de acceso. La cual se guarda por configuración. Esta lista de acceso se organiza en secciones, cada sección tiene una expresión escrita en función de servicios. ¿Porqué servicios? Porqué así no está limitada a la información que da autenticación, eso permite expresar cosas como que solo se tiene acceso en un rango de horas, por ejemplo. Además cada sección tiene una lista de presentadores permitidos.
Cuando enrutamiento pregunta si el usuario tiene permiso para acceder a un determinado presentador, control de acceso debe recuperar la sesión del usuario, consultar y hacer disponible la información del usuario, luego compilar la lista de presentadores disponibles y buscar el que solicita enrutamiento.
En teoría es posible hacerlo al contrario: buscar en donde aparece el presentador en las listas de control de acceso, y luego verificar. Pero para hacer esto hay que leer todas las listas de control de acceso. Esta alternativa solo será útil si se cambia la estructura de la lista de control de acceso para que tenga presentadores primero y luego condiciones... pero en la practica es más difícil el mantenimiento en esta estructura.
Validación de entrada
La validación de entrada ocurre en dos momentos: en el lado del cliente y en el lado del servidor.
Ocurre en el lado del cliente para responder rápidamente y ahorrarnos una petición innecesaria, ocurre en el servidor porque es posible saltarse la verificación del lado del cliente.
Es necesario que ambas verificaciones coincidan... y es posible que estas verificaciones debamos hacerlas en lenguajes diferentes. La solución es tener una descripción de la validación que podamos interpretar en ambos lenguajes. Esta descripción puede que la escribamos en el presentador o que la recuperamos de configuración. En cualquier caso habrá una librería en cada lenguaje que la interprete y la ejecute.
Es de destacar que se deben tener, además, mensajes de error para cada caso.
Otros
Lo que he descrito hasta ahora es el núcleo del marco de trabajo. Hay algunas otras cosas útiles (utilizarías si se quiere) - estas pueden ser o no ser estándar o hechas por terceros. Además no todos los proyectos las necesitan. A conocer:
- Una librería para inicialización diferida.
- Una librería para manipular listas.
- Una librería para procesamiento sintáctico (similar a lo que hace un compilador).
- Una librería para plantillas de elemento (widgets).
- Una librería para manipular cadenas de texto.
Entre las plantillas de elementos estos son bastante útiles: los menús basados en la lista de control de acceso (se pueden utilizar meta datos para configurar la presentación del menú), y tablas y listas desplegables que puedan recuperar datos de la base de datos. Además es útil tener una plantilla de elemento para representar "cargando" y utilizarla cuando se hace una petición ajax. Y no hay que olvidar las cajas de texto con auto completar.
Addendum: Cada plantilla de elemento contribuirá un fragmento de HTML, de CSS y de Javascript, además del código del lado del servidor. Es útil tomar los fragmentos de CSS y Javascript de todas las plantillas de elemento de un presentador y compilarlas en un par de archivos CSS y Javascript donde se reúne el código de todas las plantillas de elemento que usa el presentador (teniendo cuidado de mantenerlo al día en caso de cambios en el presentador o las plantillas). Estos archivos, se pueden servir para pre-carga en la plantilla (no de elemento, la principal) y se pueden mandar por HTTP/2.
Trabajador de Servicio
El service worker, es un script que reside del lado del cliente y puede interceptar todas las peticiones que van del cliente al servidor. Este permite un control más preciso del cache del lado del cliente.
He aquí un uso interesante: Solicitar al servidor imágenes del tamaño exacto, y el servidor un pre-procesador puede crear versiones reducidas de imágenes y devolver la que más se aproxima al tamaño solicitado. Si, yo sé que existe srcset, a diferencia de eso, esta solución es completamente transparente.
Comentarios
Publicar un comentario