6. Arquitectura y desarrollo
6.3. Atributos de calidad
6.3.1. Modificabilidad
Este atributo fue de vital importancia debido a la intención de la empresa Effectus Software de mantener y evolucionar el sistema una vez finalizado el proyecto académico.
Por tanto, sabiendo que indudablemente se iban a realizar modificaciones una vez transferida la propiedad del código fuente al cliente, el equipo hizo foco en aplicar prácticas, patrones y técnicas de modificabilidad con el objetivo de minimizar el costo y el riesgo de hacer cambios.
Backend - Arquitectura en capas separada por dominios
El equipo decidió utilizar una variante de la Arquitectura de Capas para implementar el backend intentando favorecer la modificabilidad de este: ya sea para agregar o corregir funcionalidades e incluso potenciar la posibilidad de extracción de algún dominio del sistema hacia su propio servicio en caso de que a futuro esto fuera requerido.
Para eso, el servidor API está implementado con la siguiente estructura:
86 Ilustración 20 - Estructura del servidor API
Donde se aprecian algunos componentes generales de uso común:
“auth”, “config”, “docs”, “middlewares”, “routes” y “utils”.
Y luego un conjunto de entidades de dominio o lógica de negocio:
“assessments”, “challenges”, “organizations”, “results” y “users”.
Luego, cada entidad de dominio está internamente compuesta por el mismo conjunto de módulos que solo consume de la siguiente capa en el stack:
rutas, validaciones sobre rutas, controladores, servicios y modelos.
A continuación, se presenta el diagrama de módulos de la entidad Usuario para ejemplificar la estructura interna de cada entidad de dominio.
Ilustración 21 - Diagrama de módulos de la entidad users en el backend Dentro de los objetivos de usar esta arquitectura se encuentra:
87
• Utilizar un tipo de arquitectura simple y conocida por la comunidad Node.js que facilite la transición a nuevos desarrolladores del sistema.
• Separar claramente las responsabilidades de cada módulo del sistema encargado de resolver una petición HTTP. De forma de aumentar la probabilidad de que un desarrollador nuevo pueda identificar fácilmente dónde debe trabajar para incorporar una funcionalidad o corregir un defecto.
• Comunicación en un solo sentido entre componentes, de manera de minimizar el acoplamiento.
• Favorecer la testeabilidad, dado que cada capa puede ser testeada independientemente en base a entradas y salidas esperadas.
• Por último, dada la posibilidad de que en un futuro la empresa cliente provea el sistema como SaaS (Software as a Service) para distintas organizaciones, el equipo tomó la decisión de que cada entidad del dominio se encuentre auto contenida en su propio paquete y solamente exponga su capa de servicios para el resto del sistema.
En el eventual futuro, donde por un tema de escala, sea necesario descomponer el sistema en servicios que se desplieguen independientemente, ya desde el origen del proyecto se cuenta con una primera separación lógica de los componentes favoreciendo la potencial evolución del monolito actual a un sistema de microservicios.
Frontend - Módulos estructurados en base a su vista asociada
Para estructurar los módulos del frontend, el equipo decidió utilizar una de las estrategias recomendadas por React conocida como “Agrupación por funcionalidad o ruta” [28]. El beneficio de esta es contar con un mapeo cercano a 1 a 1 entre los componentes que se pueden identificar visualmente al utilizar la aplicación y su estructura en código.
88 Para ello, además se decidió utilizar una nomenclatura que favorezca la identificación de módulos en base a su responsabilidad.
A modo de ejemplificar esta estrategia, para las funcionalidades relacionadas al manejo de usuarios se cuenta con:
• UsersListView
o Es el componente responsable de presentar la vista de listado de usuarios y está asociada al contenido de la ruta “/users”.
o Dentro suyo contiene el componente responsable del manejo interno de listado de usuarios: UsersList.
• UsersList
o Es responsable de manejar la lógica interna del listado y permitir acciones sobre el mismo: en particular la acción de agregar un nuevo usuario al sistema.
• UserView
o Es el componente responsable de presentar la vista de un usuario y está asociada al contenido de la ruta “/users/<id>”.
o Dentro suyo contiene el componente responsable del manejo interno de los detalles de un usuario: UsersDetails.
• UserDetails
o Es responsable de manejar la lógica interna de un usuario y permitir acciones sobre el mismo: en particular las acciones de editar los detalles de un usuario y de definir los detalles de un nuevo usuario al momento de su creación.
A continuación, se muestra un diagrama de módulos para esta entidad de dominio.
89 Ilustración 22 - Diagrama de módulos de users en frontend
Como se ve en la ilustración, además de los módulos asociados a las vistas de usuario, se cuenta con un paquete de servicios.
Si bien el mismo no está asociado a una ruta en concreto, sí está asociado a una funcionalidad: proveer una interfaz común para la interacción con la API.
Para eso se implementó un servicio abstracto y genérico, denominado CommonService, que define las interacciones comunes que todas las entidades realizan con la API.
Estas son:
• Crear una entidad de tipo genérico dado.
• Actualizar una entidad de tipo genérico dado.
• Obtener una entidad de tipo genérico dado.
• Obtener el listado de entidades de tipo genérico dado.
• Clonar entidad de tipo genérico dado.
• Borrar entidad de tipo genérico dado.
Y luego, cada entidad se encarga de proveer la implementación de dichas operaciones para un tipo concreto en base a la ruta correspondiente en el backend y los detalles de implementación propios de cada entidad. En la anterior
90 ilustración, se ve representada la implementación concreta para la entidad de tipo usuario de nombre userService.
Habilidad de modificar el comportamiento del sistema sin nuevo despliegue
En determinados lugares del sistema, el equipo aplicó tácticas de modificabilidad que implican mover ciertas configuraciones fuera del propio código fuente. Estas configuraciones, definidas como variables de configuración, permiten modificar el comportamiento del sistema sin la necesidad de realizar un nuevo despliegue.
A modo ilustrativo, se presentan a continuación las variables configuradas para el backend.
Ilustración 23 - Variables de configuración
Gracias a esto también se puede testear un mismo código de aplicación en diferentes ambientes, mediante configuraciones distintas. Un ejemplo de ello es el uso de variables de configuración para definir a qué base de datos el sistema debe conectarse, teniendo diferentes conexiones según ambiente de ejecución.
91 Además de estas variables de configuración, el equipo también utilizó la técnica de Defer Binding [27] para implementar la solución al principal desafío del proyecto: ejecución de código de terceros.
El objetivo de esta técnica es diferir lo máximo posible la toma de decisiones sobre qué implementación de un módulo se debe incluir en la ejecución del sistema. Y dicha decisión puede ser parametrizada y combinada con el uso de variables de configuración para alternar entre implementaciones también sin necesidad de un nuevo despliegue.
En particular, se aplicó Defer Binding en combinación con el patrón de diseño Strategy [29] para implementar las dos estrategias de ejecución de código de terceros presentes en el sistema y dejar la puerta abierta a implementar nuevas versiones en el futuro.
Para implementarlo, se definió una interfaz de nombre EvalWrapper que contiene dos métodos:
• RunCodeInSafeContext
o Encargada de la ejecución del código del candidato en un contexto seguro.
• RunTestsInSafeContext
o Encargada de la ejecución de los casos de prueba junto con el código del candidato en un contexto seguro.
Luego, se implementaron distintas estrategias que implementan dicha interfaz a medida que el proyecto fue avanzando:
• ContextEvalWrapper
o En la primera iteración del sistema se implementó la interfaz EvalWrapper utilizando una librería capaz de generar un contexto seguro de ejecución de código en base a iframes, con la
92 particularidad que la ejecución de este bloquea la UI. Por tanto, estaba pensada inicialmente para cómputos muy pequeños de código que tomaran apenas unos milisegundos en ser ejecutados.
• WorkerEvalWrapper
o Como mejora, en una segunda iteración del sistema, se implementó la interfaz EvalWrapper utilizando la API de Web Workers que permite la ejecución de código de manera segura con el beneficio de que no bloquea la UI debido a que se realiza en un hilo independiente del principal.
Esta implementación del patrón Strategy favorece la modificabilidad, permitiendo implementar nuevas estrategias con muy bajo impacto en el resto del sistema.