En el siguiente artículo vamos a crear un pequeño cliente de Hacker News utilizando Phoenix Framework y Surface UI (LiveView), aplicando los conceptos de MVVM (Modelo - Vista - Vista Modelo) para el diseño de la arquitectura.
Los conceptos están basados en los artículos de Matteo Manferdini quien se enfoca en la tecnología móvil SwiftUI, la cual es muy similar a las tecnologías declarativas como LiveView.
El patrón MVVM incorpora buenas ideas y algunas dificultades debido a las distintas interpretaciones del mismo. En este artículo veremos sus ventajas y como navegar sus desafíos.
Requisitos
Para poder seguir este artículo recomendamos tener instalado Elixir y configurado un proyecto con SurfaceUI.
Para tener un ejemplo de MVVM en LiveView, vamos a crear una pequeña aplicación para Hacker News, un portal de noticias para devs. Vamos a utilizar su API para obtener diez noticias desde la sección de mejores historias.
La tecnología actual permite crear aplicaciones complejas con relativa sencillez, lo que ha facilitado algunas personas a utilizar prácticas que dificultan la mantenibilidad y robustez de las soluciones de software.
Para lograr productos de software robustos y fáciles de mantener en el tiempo, se necesita más que juntar piezas de código esparcidas sin un orden cohesivo. Si bien uno puede buscar en Google para resolver tareas específicas, copiando y pegando el código para que funcione de alguna forma. Al momento de salir de lo básico y entrar al terreno profesional, inevitablemente encontraremos dificultades.
Por esta razón la industria ha desarrollado patrones como el MVC y el MVVM.
¿Qué es el patrón MVC?
El patrón Modelo-Vista-Controlador (MVC), es uno de los primeros que deberías aprender. Es tan fundamental que ha sobrevivido décadas en la industria y sus ideas se han esparcido por muchas plataformas. Es el padre de muchos otros patrones derivados como MVVM, entre otros.
Este patrón es esencial debido a que ayuda a responder una de las preguntas más comunes:
¿Dónde debería poner esta pieza de código?
El patrón MVC es uno de arquitectura. Te entrega un mapa de la estructura de la aplicación y como su nombre dice, consiste en tres capas.
Capa modelo (model): Es la capa que maneja los datos y la lógica de negocios, independiente de su representación visual.
Capa vista (view): Es la capa que muestra la información al usuario y permite interacciones, independiente de la capa de datos.
Capa controlador (controller): Es la capa que actúa como puente entre modelo y vista. Almacena y manipula el estado de la aplicación y proporciona datos a las vista, interpreta las acciones del usuario según las reglas de negocio.
El siguiente diagrama de Apple muestra un poco la relación de las vistas y controladores.
El principal problema de MVC y por qué razón nacieron otros patrones derivados es debido a la tendencia de que los controladores crecían de forma exponencial. Incluso llegando a ser llamado Massive View Controllers, por la cantidad de responsabilidades que tenían que cumplir.
¿Qué es el patrón MVVM?
El patrón Modelo-Vista-VistaModelo (MVVM), es un patrón de arquitectura que facilita estructurar la aplicación dividiéndola en tres roles.
El modelo (model): representa los datos y lógica de negocio de la aplicación.
La vista (view): Muestra la información al usuario y permite la interacción.
La vista-modelo (view-model): Actúa como puente entre las capas de vista y modelo. Contiene el estado de la vista y maneja la lógica de interacciones.
¿Diferencias entre MVC y MVVM?
Al comparar los patrones de MVC y MVVM es notable la similitud y son casi idénticos.
La principal diferencia radica en que MVC hace énfasis en los controladores. Encargados de manejar las interacciones para varias vistas. En cambio en MVVM la vista-modelo es un único componente que controla el comportamiento y estado de una única vista. Comúnmente representado como un componente.
Otra diferencia es la forma de comunicación entre la vista y su controlador. En MVC la vista y el controlador tienen funciones definidas que son llamadas de forma imperativa para informar sobre una acción o requerir actualizar la información en la vista. Por otra parte en MVVM la vista y la vista-modelo están unidas por un mecanismo de enlazado (binding) que automáticamente informa sobre interacciones realizadas en la vista y cambios ocurridos en la vista-modelo. Estos mecanismos de enlazado varían según la plataforma, en el caso de LiveView ya viene todo configurado de fábrica y es más simple e intuitivo.
La importancia de MVVM
El utilizar un patrón de arquitectura como MVVM con roles claramente definidos nos ayudan cumplir principios de diseño como la separación de conceptos. Lo que es una piedra angular para mantener código bien organizado, fácilmente entendible y que sus pruebas unitarias son viables de implementar.
Utilizar patrones de arquitectura como MVVM es sumamente importante. A pesar de que LiveView nos da herramientas innovadoras para elaborar nuestras aplicaciones, si no utilizamos patrones de arquitectura el código se irá acumulando, aumentando de complejidad, para finalmente crear monolitos masivos que son difíciles de mantener y probar.
El hecho de que LiveView maneje automáticamente la actualización de las vistas no justifica abandonar las buenas prácticas en el desarrollo de software que han existido por décadas en múltiples plataformas.
Las capas de MVC interactúan y son interpretadas dependiendo de algunos factores como:
La plataforma donde se implementa.
La experiencia del profesional y su interpretación del patrón.
La moda del día (Los devs igual pueden seguir modas).
El patrón Modelo-Vista-VistaModelo (MVVM) es principalmente una versión de MVC bajo un nombre diferente.
Si bien hay ligeras diferencias, perfectamente se pueden utilizar los conceptos de MVC y MVVM de forma unificada sin problemas. Para poder simplificar, solamente nos referimos como MVVM, ya que es una de las formas válidas de interpretar este patrón.
¿Por qué MVVM es ideal para LiveView?
Vamos a repasar las distintas herramientas de LiveView y de qué forma podemos extrapolarlas a los conceptos de MVVM.
Es el encargado principal de gestionar eventos y estados generales o relativos al servidor y tener un árbol de vistas y vista-modelos
LiveComponent
View-Model
Es el encargado de gestionar eventos y estados relativos a la vista y coordinar la obtención de datos desde internet/base de datos.
Component
View
Es la vista y solamente tiene propiedades para mostrar los datos entregados por la vista-modelo
Phoenix no fuerza a seguir un patrón arquitectónico explícito. Sin embargo LiveView es particularmente apropiado para el patrón MVVM. Ofrece componentes que son independiente de los datos que se integran muy bien a la capa vista del patrón MVVM. Además LiveView proporciona mecanismos para enlazar las vistas a los datos y automáticamente actualizar las interfaces de usuario cuando los datos asociados tienen cambios.
El siguiente diagrama muestra una posible organización de arquitectura siguiendo MVVM con LiveView.
Más allá de MVVM
Los patrones de arquitectura como MVC y MVVM tienen su foco en aplicaciones donde principalmente tenemos interacciones de usuario (UX), pero muchas veces las aplicaciones tienen que comunicar con servicios externos y otros elementos que necesitan otras formas de gestionar nuestra arquitectura de código.
Para esto recomendamos utilizar patrones como los definidos en el Diseño Orientado a Dominio (Domain Driven Design) y arquitectura Hexagonal.
Además de conceptos creados específicamente para la BEAM como Worker Bees y CRC.
Pero ver en mayor profundidad los conceptos de DDD y amigos quedará como tarea de auto estudio para el lector.
La infraestructura son todos aquellos servicios externos a nuestra aplicación. Acá se encontrarán los elementos que interactúan con ellos. Esto se consideraría una capa "Boundary".
api.ex y mock.ex
Parte de la infraestructura, contiene las llamadas a la API de HackerNews. No realiza ningún tipo de validación de parámetros o transformación de datos, ya que eso es responsabilidad de otros elementos. Simplemente se enfoca en llamar al servidor externo y devolver el resultado.
Notar que la base_url es modificada en el ambiente de test para utilizar una API Mock que utilizamos para validar en las pruebas.
Esto es parte de una técnica de mock que nos permite simplificar las pruebas sin acoplar nuestro cliente.
Los archivos en este contexto son los encargados de procesar las llamadas y respuestas a los componentes de infraestructura.
types.ex
Las estructuras que serán utilizadas para llamadas y respuestas. Su única responsabilidad es estandarizar los datos, validarlos y transformarlos en estructuras.
En este contexto se pensó solamente en la estructura Item, la cual procesa la respuesta de HackerNews y será usada posteriormente en la vista.
defmoduleHackerNews.Models.HackerNews.BestStories.Types.Itemdodefstruct~w(id comment_count score author title date url footnote)adefpget_footnote(json)dourl=Access.get(json,"url","")|>URI.parse()time=Access.get(json,"time",System.os_time())|>DateTime.from_unix!()%{host:url.host,time:time,by:Access.get(json,"by","unknown")}enddefdecode(json)do%__MODULE__{id:get_in(json,["id"]),comment_count:get_in(json,["descendants"]),score:get_in(json,["score"]),author:get_in(json,["by"]),title:get_in(json,["title"]),date:get_in(json,["time"]),url:get_in(json,["url"]),footnote:get_footnote(json)}endend
queries.ex
Es el encargado de realizar las distintas llamadas a la API utilizando los endpoints con GET. Este archivo es parte de CQRS (Command, Query, Responsability, Segregation). Un patrón que nos recomienda separar las consultas de las operaciones. En el caso de HackerNews solamente realizamos consultas, pero si quisieramos realizar operaciones tendríamos que tener un archivo commands.ex para las llamadas a la API del tipo POST, PUT, PATCH y DELETE.
Es el encargado de manejar los eventos de la vista y llamar a nuestros modelos para obtener información.
Notemos además como se utiliza una función para formatear los datos antes de que la vista los obtenga y muestre.
defmoduleHackerNewsWeb.HackerNews.Live.BestStories.ViewModeldouseSurface.LiveComponentaliasHackerNewsWeb.HackerNews.Live.BestStories.View.Components.EntryaliasHackerNews.Models.HackerNews.BestStoriesdataentries,:list,default:[]@impltruedefmount(socket)dosocket=socket|>assign(:entries,BestStories.top()){:ok,socket}end# This function is a small helper to have relative time.# To avoid using a library like Timex.# Extracted from: https://stackoverflow.com/a/65915005# And https://gist.github.com/h00s/b863579ec9c7b8c65311e6862298b7a0defpfrom_now_ago_in_words(later,now\\DateTime.utc_now())doseconds=DateTime.diff(now,later)minutes=round(seconds/60)caseminutesdominuteswhenminutesin0..1->casesecondsdosecondswhensecondsin0..4->"less than 5 seconds"secondswhensecondsin5..9->"less than 10 seconds"secondswhensecondsin10..19->"less than 20 seconds"secondswhensecondsin20..39->"half a minute"secondswhensecondsin40..59->"less than 1 minute"_->"1 minute"endminuteswhenminutesin2..44->"#{minutes} minutes"minuteswhenminutesin45..89->"about 1 hour"minuteswhenminutesin90..1439->"about #{round(minutes/60)} hours"minuteswhenminutesin1440..2519->"1 day"minuteswhenminutesin2520..43199->"#{round(minutes/1440)} days"minuteswhenminutesin43200..86399->"about 1 month"minuteswhenminutesin86400..525599->"#{round(minutes/43200)} months"minuteswhenminutesin525600..1051199->"1 year"_->"#{round(minutes/525600)} years"endenddefrender(assigns)do~F"""
<div id="beststories">
<h1 class="text-5xlfont-extrabolddark:text-whitemb-10">HackerNews Best Stories</h1>
{#for entry <- @entries}
<Entry
url={entry.url}
title={entry.title}
footnote={"#{entry.footnote.host} - #{from_now_ago_in_words(entry.footnote.time)} ago by #{entry.footnote.by}"}score={entry.score}comment_count={entry.comment_count}/>{/for}</div>"""
end
end
components/entry.ex
La vista esta principalmente creada usando componentes. En este caso un único componente que muestra los datos de una noticia.
Gracias a la técnica de mocks para la API nuestras pruebas solamente se concentran en evaluar si la renderización es correcta y contiene la información necesaria.
defmoduleHackerNewsWeb.HackerNews.Live.BestStoriesTestdo@moduledocfalseuseHackerNewsWeb.ConnCase,async:trueuseSurface.LiveViewTestimportPhoenix.LiveViewTestaliasHackerNews.Infra.Mocks.HackerNews.BestStories.API,as:Mock@route"/"describe"Best Stories"dotest"that displays the 10 best stories",%{conn:conn}do{:ok,liveview,html}=live(conn,@route)# first check if we have the container elementassertliveview|>element("#beststories")|>has_element?()==true# then we use Floki to parse the html{:ok,document}=Floki.parse_document(html)entries=Floki.find(document,".entry")assertEnum.count(entries)==10titles=Floki.find(document,".entry-title")|>Enum.map(fn{_htag,_hattrs,[{_atag,_aattrs,[title]}]}->titleend)asserttitles==Enum.map(Mock.data,fn{_k,v}->v["title"]end)endendend
Conclusión
El utilizar patrones como MVVM nos permite simplificar nuestra organización de código, mejorar la experiencia al crear pruebas y tener cierta estandarización en los proyectos.
Sin embargo no son los únicos patrones que podemos utilizar, ya que los proyectos de Phoenix van mucho más allá que las interfaces de usuario, tenemos a nuestra disposición todo un ecosistema unificado de frontend y backend.
Nuestras aplicaciones tienen que responder las siguientes preguntas, según el patrón CRC:
Crear: ¿Cómo se crean/obtienen los datos?.
Reducir: ¿Qué transformaciones necesitan y cómo se deben hacer?.
Consumir: ¿Cómo muestro el resultado o consumo dicho dato?.
Siguiendo estos conceptos podremos organizar y mejorar nuestras soluciones de software para que sean robustaz, eficientes y fáciles de mantener en el tiempo.