A medida que una aplicación crece, se vuelve más difícil de mantener. El valor de los módulos reutilizables aumenta a la par que la complejidad. Sabemos que tenemos que hacer algo para poder manejar esta complejidad, antes de que nos lleve al fracaso.
¡Patrones de diseño al rescate!
Aplicaciones complejas
Una aplicación compleja se caracteriza por tener al menos una de las siguientes características:
- Múltiples componentes en el árbol de componentes que muestran el mismo fragmento del estado de la aplicación.
- Varias fuentes de actualización del estado como pueden ser:
— Varios usuarios interactuando a la vez.
— Sistemas backend que envían actualizaciones del estado al navegador en tiempo real.
— Tareas en segundo plano programadas.
— Sensores de proximidad u otro tipo de sensores.
- Actualización del estado de la aplicación muy frecuente.
- Un gran número de componentes.
- Componentes con muchas líneas de código, similares a aquellos controladores Big Ball of Mud AngularJS de antaño.
- Alto nivel de complejidad ciclomática presente en los compoentes — una elevada concentración de ramas lógicas o flujos de control asíncronos.
Todos queremos tener una aplicación que sea mantenible, testeable, escalable y optimizada, todo a la vez.
Las aplicaciones complejas rara vez poseen todas las características de mayor valor. No podemos evitar todas las características complejas a la par que cumplimos los requisitos avanzados del proyecto, pero sí podemos diseñar nuestra aplicación para que se magnifiquen sus características más valiosas.
Separación de conceptos
Podemos pensar en la separation of concerns/separación de conceptos como la compartimentalización de nuestra aplicación. Agrupamos la lógica por conceptos de sistema para poder centrarnos en individualmente en cada concepto. En el nivel superior, la separación de conceptos es una disciplina arquitectural. Aplicado al desarrollo de nuestro día a día, es saber prácticamente de memoria qué lugar ocupa cada elemento.
Podemos trocear nuestras aplicaciones vertical u horizontalmente, o de ambas formas. Cuando troceamos verticalmente, agrupamos los artefactos de software por feature/funcionalidad. Cuando troceamos horizontalmente, agrupamos por capas de software. En nuestras aplicaciones, podemos categorizar los artefactos de software en estas capas horizantales, o asuntos de sistema:
Podemos aplicar las mismas normas a nuestros componentes de Angular, que solo deberían estar centrados en las capas de presentación y de interacción de usuario. El resultado de aplicar esto es que minimizamos el acoplamiento entre las partes móviles de nuestro sistema.
Es cierto que este proceso require mucha disciplina, ya que añadimos capas adicionales de abstracción, pero el resultado hace que merezca la pena el esfuerzo. Hemos de tener en cuenta que vamos a tener que crear capas de abstracción que deberían haber estado ahí desde el principio.
El patrón Modelo-Vista-Presentador
Modelo-Vista-Presentador (MVP) es un patrón de diseño arquitectural para implementar la interfaz de usuario (UI) de una aplicación. Se utiliza para minimizar lógica compleja en las clases, funciones y módulos (artefactos de software) que son difíciles de testear. En particular, evitamos complejidad en artefactos específicos de UI, como los componentes de Angular.
Como el Modelo-Vista-Controlador — patrón del que deriva — Modelo-Vista-Presentador separa la presentación del dominio del problema. La capa de presentación reacciona a los cambios del dominio mediante la aplicación del Patrón Observer, tal y como han descrito Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides (también conocidos como “La banda de los cuatro”) en su libro “Design Patterns: Elements of Reusable Object-Oriented Software”.
En el Patrón Observer, un subject mantiene una lista de observers a los que notifica cuando ocurre un cambio de estado. ¿Os suena? Lo habéis adivinado, RxJS está basado en el Patrón Observer.
La vista no contiene lógica ni comportamiento ninguno, exceptuando los data bindings y la composición de widgets. Cuando ocurre alguna interacción del usuario, se delega el control a un presentador.
El presentador acumula cambios de estado para que la acción de un usuario rellenando un formulario conlleve a un gran cambio de estado, en contraposición a muchos cambios pequeños. Ej: Actualiza el estado de la aplicación una vez por formulario en lugar de una vez por campo del formulario. Esto hace que sea más sencillo el deshacer/rehacer cambios de estado. El presentador actualiza el estado emitiéndo un comando al modelo. El cambio de estado se refleja en la vista gracias a la Sincronización Observer.
La variante de Angular
Vamos a crear artefactos software inspirados en los patrones y variaciones originales del Modelo-Vista-Presentador que encajen con la plataforma de Angular y su elemento de construcción de UI clave: el componente.
Idealmente, un componente de Angular se centra únicamente en la presentación y la interacción del usuario. En realidad, tenemos que mantener una disciplina estricta para asegurarnos de que nuestros componentes se centran solo en presentarle una parte del estado de la aplicación al usuario, permitiendo que este afecte a ese estado.
La variación Modelo-Vista-Presentador tratada en este artículo es una versión del Encapsulated Presenter Style/Estilo de Presentador Encapsulado. Sin embargo, nuestros presentadores no tendrán ninguna referencia a sus vistas respectivas, permitiendo que puedan testearse de forma aislada.
Al aplicar el patrón Modelo-Vista-Presentador, tendemos a utilizar el enfoque de Supervising Controller/Controller Supervisor. Nuestras vistas (componentes de Angular) dependen en su presentador para las interacciones del usuario. Dado que nuestros presentadores están encapsulados por sus respectivas vistas, tanto datos como eventos fluirán a través del modelo del componente en algún momento.
Con la ayuda del modelo del componente, nuestro presentador traduce la interacción del usuario a un evento específico del componente. Este evento se traduce a su vez en un comando que se envía al modelo. La última traducción se maneja por los componentes contenedor, de los que hablaremos en breve.
Nuestro presentador tendrá algunas de las características de un Presentation Model/Modelo de Presentación. Por ejemplo, tendrá cierta lógica de presentación: una propiedad booleana u observable para indicar si un elemento del DOM tiene que ser deshabilitado o no, o una propiedad que indique de qué color se tiene que renderizar un elemento del DOM.
Nuestra vista enlaza con las propiedades del presentador para proyectar el estado que reperesenta sin necesidad de tener lógica adicional. El resultado es un modelo del componente ligero, con un template muy simple.
Conceptos Modelo-Vista-Presentador para Angular
Para aplicar el patrón Modelo-Vista-Presentador en una aplicación Angular, tenemos que hablar de algunos conceptos que están muy basados en la comunidad de React. Nuestros componentes pertenecerán — en el contexto de este artículo — a una de estas tres categorías:
- Componentes de presentación
- Componentes contenedor
- Componentes mixtos
Los desarrolladores de React han estado extrayendo componentes de presentación y componentes contenedor de componentes mixtos durante varios años. Podemos utilizar los mismos conceptos en nuestras aplicaciones Angular. Además, añadiremos el concepto de presentadores.
Componentes de presentación
Los componentes de presentación son vistas puramente interactivas y de presentación. Presentan una parte del estado de la aplicación al usuario, permitiendo que este afecte a dicho estado.
A excepción de los presentadores, los componentes de presentación no son conscientes de las demás partes de la aplicación. Tienen una API de data binding que describe las interacciones del usuario que pueden manejar y los datos que necesitan.
Para evitar la mayoría de las razones por las que se realizan tests de UI, vamos a limitar la complejidad de los componentes de presentación a un mínimo estricto. Esto incluye tanto el modelo del componente como el template del componente.
Componentes contenedor
Los componentes contenedor exponen partes del estado de la aplicación a los componentes de presentación. Integran la capa de presentación con el resto de nuestra aplicación mediante la traducción de los eventos específicos de los componentes a comandos/queries para las demás capas.
Normalmente, tenemos una relación de uno a uno entre un componente contenedor y un componente de presentación. El componente contenedor tiene propiedades de clase que corresponden a las propiedades input del componente de presentación, y métodos que responden a los eventos que se emiten a través de las propiedades output del componente de presentación.
Componentes mixtos
Si un componente no es un componente contenedor ni un componente de presentación, es un componente mixto. Dada una aplicación existente, es muy probable que esté compuesta por componentes mixtos. Los llamamos componentes mixtos porque tienen conceptos de sistema mezclados: contienen lógica que pertenece a capas horizontales diferentes.
No hay que sorprenderse si nos encontramos con un componente que, además de contener un array de objetos de dominio para la presentación, accede directamente a la cámara del dispositivo, realiza llamadas HTTP y cachea el estado de la aplicación mediante WebStorage.
Aunque es cierto que una aplicación debe contener esta lógica, agruparla en un solo lugar hace que sea complicada de testear, difícil de entender y casi imposible de reutilizar. Está fuertemente acoplada.
Presentadores
La lógica de comportamiento y la lógica de presentación compleja se extraen a un presentador para poder tener componentes de presentación simples. El presenter carece de UI, y no suele tener dependencias inyectadas, lo que hace que sea sencillo de testear y entender.
El presenter rara vez es consciente del resto de la aplicación. Normalmente, el presenter se referencia por un solo componente de presentación.
La tríada Modelo-Vista-Presentador
Estos tres artefactos de software se combinan en lo que conocemos como la tríada Modelo-Vista-Presentador:
El modelo, representado por los componentes contenedor, es el estado de la aplicación que se muestra al usuario para que este pueda alterarlo y navegar por él.
La vista, representada por los componentes de presentación, es una interfaz de usuario ligera que presenta el estado de la aplicación y traduce las interacciones de usuario a eventos específicos de componente, a menudo redirigiendo el flujo de control al presentador.
El presentador suele ser una instancia de una clase que no es consciente del resto de la aplicación.
Flujo de datos
Vamos a visualizar el flujo de datos y eventos a través de una tríada Modelo-Vista-Presentador
Flujo de datos bajando por el árbol de componentes
En la Figura 2, hay un cambio de estado de la aplicación que ocurre en un servicio. Se notifica al componente contenedor de este cambio, ya que está suscrito a una propiedad observable del servicio.
El componente contenedor transforma el valor emitido de la manera más conveniente para que el componente de presentación pueda utilizarlo. Angular asigna nuevos valores y referencias a las propiedades input enlazadas en el componente de presentación.
El componente de presentación le pasa los datos actualizados al presentador, que recomputa las propiedades adicionales utilizadas en el template del componente de presentación.
Los datos han terminado de fluir a través del árbol de componentes y Angular se encarga de renderizar el estado actualizado en el DOM, mostrándolo al usuario.
Flujo de eventos subiendo por el árbol de componentes
En la Figura 3 el usuario hace click en un botón. Angular dirige el control a un manejador de eventos en el modelo del componente de presentación, debido al event binding que hay en el template del componente.
La interacción del usuario, el click, se intercepta por el presentador, que lo traduce, convirtiéndolo en una estructura de datos que después emite a través de una propiedad observable. El modelo del componente de presentación observa el cambio y emite el valor mediante una propiedad output.
Angular notifica al componente contenedor del valor emitido en el evento específico del componente, debido a un event binding en su template.
Dado que el evento ha terminado de fluir a través árbol de componentes, el componente contenedor traduce la estructura de datos a argumentos, que se le pasan a un método del servicio.
Es bastante común que, después de recibir un comando para cambiar el estado de la aplicación, un servicio emita el cambio en sus propiedades observables, y los datos vuelven a bajar por el árbol de componentes, como vimos en la Figura 2.
Una aplicación Angular mejorada
Habrá quienes consideren que nuestra arquitectura de UI será el resultado complejo producido por una sobre-ingeniería, cuando en realidad lo que nos queda son muchas piezas de software pequeñas, simples y modulares. Una arquitectura de software modular es la que nos permite que seamos ágiles. No me refiero a ágiles en el sentido de procesos y ceremonias ágiles, sino a ágiles respecto al coste del cambio.
Una arquitectura de software modular nos permite ser ágiles.
En lugar de acabar con una montaña de deuda técnica cada vez mayor, manejamos los cambios en los requisitos del cliente de manera proactiva. Si el sistema estuviese fuertemente acoplado, fuera difícil de testear o nos llevase meses poder refactorizarlo, sería muy complicado, por no decir imposible, alcanzar este nivel de agilidad.
Mantenible
A pesar de que el sistema resultante esté compuesto de muchas partes móviles, cada parte es muy sencilla y se limita a abordar un único concepto del sistema. Además, tenemos un sistema completamente transparente respecto a qué partes van en qué sitios.
Testeable
Minimizamos la lógica en los artefactos específicos de Angular, ya que son complicados y lentos de testear. Como cada parte del software se centra únicamente en un concepto del sistema, son fáciles de entender y de testear automáticamente.
La UI es particularmente difícil y lenta de testear y Angular no es una excepción a la regla. Mediante el patrón Modelo-Vista-Presentador, minimizamos la cantidad de lógica en componentes de presentación hasta el punto en el que apenas merece la pena hacerles tests. En su lugar, podemos evitar el hacerles tests unitarios y depender de nuestros tests de integración y test end-to-end para encontrar los pequeños errores de sintaxis, de tipografía o de propiedades sin inicializar.
Escalable
Se pueden desarrollar nuevas funcionalidades de manera independiente. Los artefactos de capas horizantales separadas también pueden desarrollarse y testearse de forma aislada. Somos perfectamente conscientes de dónde va cada parte de lógica.
Ahora que podemos desarrollar las capas independientemente, podemos distinguir entre desarrollo front-end visual y técnico. Hay desarrolladores a los que se les da genial implementar el comportamiento mediante RxJS, otros a los que les encanta la integración con el back-end, y otros a los que les gusta perfeccionar el diseño y ocuparse de las cuestiones de accesibilidad mediante CSS y HTML.
Además, al desarrollar cada funcionalidad de forma aislada, podemos dividir las tareas entre equipos. Un equipo se puede ocupar del nuevo catálogo de productos, mientras que otro equipo se encarga del mantenimiento y de añadir nuevas funcionalidades al carrito de la compra en un sistema de e-commerce.
Eficiente
Una aplicación con separación de conceptos bien hecha suele ser altamente eficiente, especialmente en la capa de presentación, ya que es fácil localizar y aislar los cuellos de botella de eficiencia.
Con la estrategia de detección de cambios OnPush
, podemos minimizar el impacto que los ciclos de detección de cambios de Angular tienen en la eficiencia de nuestra aplicación.
Caso de estudio: Tour of Heroes
Empezamos donde el tutorial “Tour of Heroes” de Angular.io termina. Se suele utilizar como punto de partida, ya que es un tutorial comúnmente conocido por los desarrolladores de Angular.
Todos los componentes que hay en el código del Tour of Heroes son componentes mixtos. Este hecho es obvio, considerando que, aunque ninguno tiene propiedades output, algunos modifican el estado de la aplicación.
En los artículos relacionados, aplicaremos el patrón Modelo-Vista-Presentador a una selección de estos componentes, paso a paso, con muchos ejemplos. Además, hablaremos sobre los comportamientos de la tríada Modelo-Vista-Presentador a los que hay que hacerles test.
Os podréis dar cuenta de que no estamos cambiando la funcionalidad ni el comportamiento de la aplicación, sino que estamos refactorizando los componentes para convertirlos en artefactos especializados.
Mientras que estos artículos solo tratan algunos de los componentes del Tour of Heroes, he aplicado el patrón Modelo-Vista-Presentador a la aplicación entera, además de añadir suites de test para los componentes contenedor y los presentadores. Podéis ver el código en este repositorio de GitHub.
Requisitos
Además de los conceptos presentados en este artículo, es necesario conocer algunos conceptos clave de Angular. Los conceptos del Modelo-Vista-Presentador están explicados en detalle en los artículos relacionados.
Hay que estar familiarizad@ con los componentes de Angular: sintaxis del data binding además de propiedades input and output. También se asume un conocimiento básico de RxJS — observables, subjects, operadores y suscripciones.
Construiremos tests unitarios aislados en los que falsearemos las dependencias de los servicios mediante espías de Jasmine. La comprensión de los stubs y demás test doubles no es necesaria para entender los tests. Nos centraremos en los tests en sí e intentaremos entender por qué los hacemos.
Recursos
Podéis ver el código completo del tutorial Tour of Heroes en StackBlitz.
Podéis descargar el código completo del tutorial Tour of Heroes (zip archive, 30 KB)
Podéis ver el Tour of Heroes — estilo Modelo-Vista-Presentador en el repositorio de GitHub.
Podéis ver mi charla “Model-View-Presenter with Angular” del meetup de ngAarhus de May 2018:
Las diapositivas de mi charla “Model-View-Presenter with Angular”:
Artículos relacionados
Podéis leer más acerca de cómo el patrón Modelo-Vista-Presentador y su hermano el patrón Modelo-Vista-Controller se introdujeron en los frameworks de UI del lado del cliente aquí.
¿Estás harto de preocuparte por el manejo de estado y otros temas del back-end en tus componentes de Angular? Extrae toda la lógica que no sea de presentación a componentes contenedor. Puedes leer cómo hacerlo en “Container components with Angular”.
Aprende cómo hacer tests a la lógica del componente contenedor con test unitarios veloces en “Testing Angular container components”.
En “Presentational components with Angular” hablamos sobre componentes puros, deterministas y potencialmente reutilizables que únicamente dependen de las propiedades input y los eventos disparados por la interacción del usuario para determinar su estado interno.
Aprende cómo extraer un presentador en “Presenters with Angular”.
En “Lean Angular components”, comentamos la importancia de una arquitectura de componentes robusta. El patrón Modelo-Vista-Presentador encapsula varios de los patrones que nos permiten alcanzarla.
Agradecimientos
Los diagramas de flujo animados han sido creados por mi buen amigo y compañero de profesión Martin Kayser.
El concepto de intentar lograr la separación de conceptos está inspirado por las obras de Robert “Uncle Bob” Martin, en particular, su libro “Clean Architecture: A Craftsman’s Guide to Software Structure and Design”.
El aplicar el patrón Modelo-Vista-Presentador en una aplicación Angular ha sido inspirado por el artículo “Model View Presenter, Angular, and Testing” por Dave M. Bush.
En mi investigación preliminar, estudié el patrón Modelo-Vista-Presentador para JavaScript vainilla descrito en el artículo “An MVP guide to JavaScript — Model-View-Presenter” por Roy Peled.
Editor
Me gustaría agradecer a Max Koretskyi por haberme ayudado a que este artículo estuviese lo mejor posible. Agradezco el tiempo que dedica a compartir sus experiencias acerca de escribir para la comunidad de desarrollo de software. Además, muchas gracias por publicar mis artículos para que los pueda compartir con el público de Angular INDEPTH.
Revisores
Muchas gracias, queridos revisores, por ayudarme a llevar a cabo este artículo. ¡Vuestro feedback no tiene precio.!
Nota de la editora: Este artículo ha sido publicado originalmente por Lars Gyrup Brink Nielsen en dev.to: Model-View-Presenter with Angular
Traducción por Estefanía García Gallardo.