Patrones de diseño comunes en Ruby on Rails

Patrones de diseño comunes en Ruby on Rails

No importa el nivel en el que te encuentres. Seguramente sabes que Ruby on Rails es un framework MVC, por lo que asumo que entiendes que el Modelo Vista, controlador en el que Rails se basa, es un Patrón de Diseño muy común en Desarrollo Web. Inclusive, muchos otros frameworks usan este patrón: Laravel, ASP.NET MVC, Django, etc.

Pero aunque todos los frameworks utilizan esta forma de organizar código, su comunidad tiene patrones, llamémosles hijos de este patrón, a los que nombran y usan de forma diferente. Este artículo aborda estos patrones de diseño para la comunidad de Rails.

Pero primero definamos: ¿qué es un Patrón de Diseño?

Los patrones de diseño son soluciones (probadas) a problemas de diseño concretos (bajo cierto contexto). Proveen ciertas ventajas:

  • Alta calidad de software.
  • Más productividad.
  • Mejoran la comunicación del equipo.
  • Mejoran la habilidad de diseño en programadores no experimentados.

Los patrones de diseño son para el diseño lo que algoritmos son para la programación. Ambos proveen soluciones para problemas concretos.

Quicksort provee una solución para el ordenamiento, mientras que el patrón Observer es ideal para enviar actualizaciones para componentes de software interesados en recibirlas.

Por ejemplo, ¿cómo diseñarías tu código para una aplicación web que tenga una interfaz de usuario desde la cual podemos capturar y obtener información de una base de datos?

Primero, separas en dos partes o dos problemas a resolver. El primer problema a resolver es la parte que captura y consulta información de la base de datos: el modelo. El segundo problema está definido por la parte de tu software que muestra dicha información: la vista. Y un tercer problema, que surgirá después es la parte que conecta la vista con el modelo: el controlador.

Como seguramente notaste, lo que acabamos de describir es el patrón MVC.

Fat Controller y Fat Model

Fat Controller: Tu primer problema de diseño en Rails

Después de tus primeras miles de líneas de código en Rails (o quizá en cualquier otro framework MVC) vas a notar que, conforme tu funcionalidad crece, tus controladores se vuelven más y más grandes (gordos), tanto en número de líneas como en número de responsabilidades que tienen. Muchas veces esto genera tener mucho código duplicado por mantener y principalmente: lógica de negocio definida en el controlador.

Para ejemplificar lo anterior, utilizaremos el siguiente caso, seguido de sus respectivos diseños de clases/objetos y código de Rails.

  • Dado un monto de reembolso, una fecha y un folio, debemos reembolsar la cantidad especificada del monto a un movimiento en el sistema de tarjeta de crédito.
  • Al realizar el reembolso, el equipo de reembolsos debe de ser notificado.
  • Completado lo anterior, el cliente debe ser notificado.

El siguiente diagrama muestra a grandes rasgos el flujo, así como el Fat Controller:


Este código muestra mejor lo anterior.

Este es tu primer problema. Pero, ¿por qué?

El problema se da cuando empiezas a tener mucha lógica de negocio en el controlador. esta clase deja de tener una sola responsabilidad y de conectar la vista con el modelo, por lo que ahora empieza a ser responsable de detalles como:

  • Validar el input del usuario.
  • Enviar correos de bienvenida.
  • Extraer información de tabla A para conectarla con Tabla B y almacenarla en tabla C.

Nuestro siguiente paso natural quizá sea mover toda esta lógica del controlador al modelo. Ingresa aquí para conocer más.

Ahora nuestro FatController ya es SkinnyController, pero nuestro Model ahora es un FatModel, ¡vaya dilema!

Fat Model: Tu segundo problema

Un Fat Model empieza cuando decides agregar métodos, ya sea por hacer refactor de tu código en un controlador o simplemente porque decides que es una buena idea colocar lógica de negocio en el modelo (heredado de ActiveRecord).

Recordemos que ActiveRecord es en sí un patrón utilizado para definir la estructura de una tabla en clases, que a su vez son usadas para almacenar e incluso modificar el contenido de dicha tabla (leer, agregar, modificar, eliminar).

El problema con agregar lógica de negocio al Modelo (una clase heredada de ActiveRecord) es que poco a poco se empieza a alejar de la función principal de la clase: leer, agregar, modificar, eliminar y de añadiduras propias de Rails como validaciones, callbacks, scopes, etc. Debido a lo anterior,tu modelo ahora posee muchas responsabilidades. A esto se le llama FatModel.

Service Objects

Una vez definido el problema,  podemos proceder con la solución. Y ésta se llama Service`*`. Pongo el `*` refiriéndome a los tres tipos de Services que podemos encontrar en diferentes comunidades de Rails: Service Object, Service Class y Application Service. Para mí, y justo para el contexto en que nos encontramos, utilizaré el término Service Object el resto del artículo para referirnos al patrón que describiremos a continuación.

Un Service Object se encarga de proveer todas las clases necesarias para ejecutar una lógica de negocio, proceso o una definición más rudimentaria: lógica o procedimiento de un script. De tal forma que dirige el flujo a través de los diferentes modelos y operaciones necesarias para llevar a cabo una o varias acciones.

En Rails, estas clases casi siempre se identifican por tener un método público. ya sea de instancia o de clase llamado call to execute. En nuestro caso, el método de instancia público será call y el objeto debe ser inicializado con todas las estructuras que se necesitan para ejecutar dicho procedimiento.

En la comunidad de Ruby on Rails se suele conocer a este tipo de clases como Service Object, pero en otras comunidades pueden denominarlo  como Service Class, Stategyo Commands*. De hecho, algunas personas de la comunidad de Rails utilizan el nombre de Commands para otro contexto, como veremos más adelante.

Dado lo anterior, podemos asumir que:

InputValidator=InputValidatorService=InputValidatorStrategy= InputValidatorCommand.

Tal cual la definición de patrón de diseño siempre viene ligada a una comunidad que lo usa y siempre puede haber patrones idénticos, pero con nombres diferentes en comunidades distintas. Incluso en una misma comunidad (como Rails) puede haber un sinfín de discusiones sobre la nomenclatura correcta de este tipo de clases.

Utilizando el caso de uso planteado, el diagrama y código lucirán de la siguiente manera:

Puedes conocerlo mejor haciendo clic aquí.

Fat Service Object

Probablemente ya hayas identificado el patrón de fallo en todo esto. Y justo es a donde quería que llegáramos. Lo único que estamos haciendo es pasar lógica gorda de una clase a otra. Muchas veces este refactor es válido y va a servirte. Sin embargo, el problema surge cuando se tienen muchos Service Objects que hacen lo mismo o que simplemente siguen un flujo procedural. Básicamente, terminamos (en lenguajes dinámicos) con un gran script dentro de una clase.

Por lo tanto, voy a tratar de definir las siguientes heurísticas para evitar que tus Service Objects se vuelvan Gordos (Fat Service Object).

Las heurísticas no son malas

Todos utilizamos heurísticas, no hay tiempo para reinventar la rueda ni tiempo para reexplorar un problema que incluso nosotros mismos ya hemos resuelto antes. En la comunidad de Ruby on Rails, el lema es convención sobre configuración. En términos coloquiales: si la comunidad ya resolvió un problema de diseño, reutilicemos su solución y su implementación. Eso es justo lo que ha hecho grande a Rails por tanto tiempo.

Por ello, voy a presentarte dos patrones más para que tu diseño sea un poco mejor, así como algunas heurísticas que te ayuden a identificar cuándo usarlo.

## Commands

Lo tomamos de la comunidad DDD, principalmente extraído del libro Domain-Driven Rails por Robert Pankowecki & Arkency Team. Se refiere a Commands son clases que realizan simples validaciones **de estructuras que no son modelos** y no requieren ninguna lógica de negocio.

Los Commands van a ser objetos que reciben como argumento de inicialización nuestro Service Object. Es recomendado utilizar un solo command por Service Object. Sin embargo, la experiencia te enseñará qué número de objetos funciona mejor para ti.

Heurística: utilízalos para definir las clases que reciben tus Service Objects. Pueden ser: Plain Old Ruby Objects o puedes emplear alguna gema: dry-rb, virtus, etc.

## Domain Objects

Son objetos que proveen una forma orientada a objetos de lidiar con lógica complicada. En lugar de utilizar un Service Object que se encargue de realizar todo un procedimiento, los Service Object delegan responsabilidades de lógica de negocio a los Domain Objects.

Heurística: utilízalos cuando un Service Object tiene muchas responsabilidades. Recuerda modelar bien este tipo de clases, las cuales muchas veces terminan siendo Plain Old Ruby Objects.

Consejos Finales

Recuerda: tu habilidad más importante es la de diseñar software. En esta entrada hablamos de algunas heurísticas y finalizamos con Domain Objects. Si lo ven desde un punto de vista fuera de Rails (o cualquier otro framework), estas son simplemente clases bien modeladas para atacar problemas dentro del dominio del negocio.

Espero haberte ayudado a poner a dieta tu aplicación en Rails.

⚠️
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Revelo.

Revelo Content Network da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.