Cree componentes de rieles elegantes con objetos Ruby antiguos simples

Publicado: 2022-03-11

Su sitio web está ganando terreno y usted está creciendo rápidamente. Ruby/Rails es su lenguaje de programación preferido. Su equipo es más grande y ha renunciado a los "modelos gordos, controladores delgados" como estilo de diseño para sus aplicaciones Rails. Sin embargo, aún no desea abandonar el uso de Rails.

No hay problema. Hoy, discutiremos cómo usar las mejores prácticas de OOP para hacer que su código sea más limpio, más aislado y más desacoplado.

¿Vale la pena refactorizar su aplicación?

Comencemos analizando cómo debe decidir si su aplicación es una buena candidata para la refactorización.

Aquí hay una lista de métricas y preguntas que suelo hacerme para determinar si mi código necesita refactorización o no.

  • Pruebas unitarias lentas. Las pruebas de unidad PORO generalmente se ejecutan rápido con un código bien aislado, por lo que las pruebas de ejecución lenta a menudo pueden ser un indicador de un mal diseño y responsabilidades demasiado acopladas.
  • Modelos FAT o controladores. Un modelo o controlador con más de 200 líneas de código (LOC) suele ser un buen candidato para la refactorización.
  • Base de código excesivamente grande. Si tiene ERB/HTML/HAML con más de 30 000 LOC o código fuente de Ruby (sin GEM) con más de 50 000 LOC, es muy probable que deba refactorizar.

Intente usar algo como esto para averiguar cuántas líneas de código fuente de Ruby tiene:

find app -iname "*.rb" -type f -exec cat {} \;| wc -l

Este comando buscará en todos los archivos con extensión .rb (archivos ruby) en la carpeta /app e imprimirá el número de líneas. Tenga en cuenta que este número es solo aproximado ya que las líneas de comentarios se incluirán en estos totales.

Otra opción más precisa e informativa es usar las stats de tareas de rake de Rails, que genera un resumen rápido de líneas de código, número de clases, número de métodos, la proporción de métodos a clases y la proporción de líneas de código por método:

 bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
  • ¿Puedo extraer patrones recurrentes en mi base de código?

Desacoplamiento en acción

Comencemos con un ejemplo del mundo real.

Supongamos que queremos escribir una aplicación que registre el tiempo para los corredores. En la página principal, el usuario puede ver los tiempos que ingresó.

Cada entrada de tiempo tiene una fecha, distancia, duración e información de "estado" relevante adicional (por ejemplo, clima, tipo de terreno, etc.) y una velocidad promedio que se puede calcular cuando sea necesario.

Necesitamos una página de informe que muestre la velocidad y la distancia promedio por semana.

Si la velocidad promedio de la entrada es mayor que la velocidad promedio general, se lo notificaremos al usuario con un SMS (para este ejemplo, usaremos la API RESTful de Nexmo para enviar el SMS).

La página de inicio le permitirá seleccionar la distancia, la fecha y el tiempo dedicado a trotar para crear una entrada similar a esta:

También tenemos una página de statistics que es básicamente un informe semanal que incluye la velocidad promedio y la distancia recorrida por semana.

  • Puede consultar la muestra en línea aquí.

El código

La estructura del directorio de la app se parece a:

 ⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

No discutiré el modelo de User ya que no es nada especial ya que lo estamos usando con Devise para implementar la autenticación.

En cuanto al modelo Entry , contiene la lógica de negocio de nuestra aplicación.

Cada Entry pertenece a un User .

Validamos la presencia de los atributos de distance , time_period , date_time y status para cada entrada.

Cada vez que creamos una entrada, comparamos la velocidad promedio del usuario con el promedio de todos los demás usuarios en el sistema y notificamos al usuario por SMS usando Nexmo (no discutiremos cómo se usa la biblioteca Nexmo, aunque quería para demostrar un caso en el que usamos una biblioteca externa).

  • muestra esencial

Tenga en cuenta que el modelo Entry contiene más que solo la lógica empresarial. También maneja algunas validaciones y devoluciones de llamadas.

entries_controller.rb tiene las principales acciones CRUD (aunque no hay actualización). EntriesController#index obtiene las entradas del usuario actual y ordena los registros por fecha de creación, mientras que EntriesController#create crea una nueva entrada. No hay necesidad de discutir lo obvio y las responsabilidades de EntriesController#destroy :

  • muestra esencial

Mientras que statistics_controller.rb es responsable de calcular el informe semanal, StatisticsController#index obtiene las entradas del usuario conectado y las agrupa por semana, empleando el método #group_by contenido en la clase Enumerable en Rails. Luego intenta decorar los resultados usando algunos métodos privados.

  • muestra esencial

No discutimos mucho las vistas aquí, ya que el código fuente se explica por sí mismo.

A continuación se muestra la vista para enumerar las entradas del usuario que ha iniciado sesión ( index.html.erb ). Esta es la plantilla que se utilizará para mostrar los resultados de la acción de índice (método) en el controlador de entradas:

  • muestra esencial

Tenga en cuenta que estamos utilizando render @entries parciales para extraer el código compartido en una plantilla parcial _entry.html.erb para que podamos mantener nuestro código SECO y reutilizable:

  • muestra esencial

Lo mismo ocurre con el _form parcial. En lugar de usar el mismo código con acciones (nuevas y de edición), creamos un formulario parcial reutilizable:

  • muestra esencial

En cuanto a la vista de la página del informe semanal, statistics/index.html.erb muestra algunas estadísticas e informa el rendimiento semanal del usuario agrupando algunas entradas:

  • muestra esencial

Y finalmente, el asistente para las entradas, entries_helper.rb , incluye dos asistentes readable_time_period y readable_speed que deberían hacer que los atributos sean más legibles para los humanos:

  • muestra esencial

Nada lujoso hasta ahora.

La mayoría de ustedes argumentará que refactorizar esto va en contra del principio KISS y hará que el sistema sea más complicado.

Entonces, ¿esta aplicación realmente necesita una refactorización?

Absolutamente no , pero lo consideraremos solo con fines de demostración.

Después de todo, si consulta la sección anterior y las características que indican que una aplicación necesita refactorización, resulta obvio que la aplicación de nuestro ejemplo no es una candidata válida para la refactorización.

Ciclo vital

Entonces, comencemos explicando la estructura del patrón Rails MVC.

Por lo general, comienza cuando el navegador realiza una solicitud, como https://www.toptal.com/jogging/show/1 .

El servidor web recibe la solicitud y usa routes para averiguar qué controller usar.

Los controladores hacen el trabajo de analizar las solicitudes de los usuarios, los envíos de datos, las cookies, las sesiones, etc., y luego le piden al model que obtenga los datos.

Los models son clases de Ruby que se comunican con la base de datos, almacenan y validan datos, ejecutan la lógica comercial y, de lo contrario, hacen el trabajo pesado. Las vistas son lo que ve el usuario: HTML, CSS, XML, Javascript, JSON.

Si queremos mostrar la secuencia del ciclo de vida de una solicitud de Rails, se vería así:

Rieles que desacoplan el ciclo de vida de MVC

Lo que quiero lograr es agregar más abstracción utilizando objetos Ruby simples y antiguos (PORO) y hacer que el patrón sea similar al siguiente para las acciones de create/update :

Formulario de creación de diagrama de rieles

Y algo como lo siguiente para list/show acciones:

Consulta de lista de diagrama de rieles

Al agregar abstracciones de POROs, aseguraremos la separación total entre las responsabilidades de SRP, algo en lo que Rails no es muy bueno.

Pautas

Para lograr el nuevo diseño, usaré las pautas que se enumeran a continuación, pero tenga en cuenta que estas no son reglas que deba seguir al pie de la letra. Piense en ellas como pautas flexibles que facilitan la refactorización.

  • Los modelos ActiveRecord pueden contener asociaciones y constantes, pero nada más. Eso significa que no hay devoluciones de llamada (use objetos de servicio y agregue las devoluciones de llamada allí) ni validaciones (use objetos de formulario para incluir nombres y validaciones para el modelo).
  • Mantenga los controladores como capas delgadas y siempre llame a los objetos de servicio. Algunos de ustedes se preguntarán por qué usar controladores, ya que queremos seguir llamando a los objetos de servicio para contener la lógica. Bueno, los controladores son un buen lugar para tener el enrutamiento HTTP, el análisis de parámetros, la autenticación, la negociación de contenido, llamar al servicio correcto o al objeto editor, capturar excepciones, formatear la respuesta y devolver el código de estado HTTP correcto.
  • Los servicios deben llamar a los objetos Query y no deben almacenar el estado. Utilice métodos de instancia, no métodos de clase. Debería haber muy pocos métodos públicos de acuerdo con SRP.
  • Las consultas deben realizarse en objetos de consulta. Los métodos de objeto de consulta deben devolver un objeto, un hash o una matriz, no una asociación ActiveRecord.
  • Evite usar ayudantes y use decoradores en su lugar. ¿Por qué? Una trampa común con los ayudantes de Rails es que pueden convertirse en una gran cantidad de funciones que no son OO, todas compartiendo un espacio de nombres y superándose entre sí. Pero mucho peor es que no hay una gran manera de usar ningún tipo de polimorfismo con los ayudantes de Rails, proporcionando diferentes implementaciones para diferentes contextos o tipos, anulando o subclasificando a los ayudantes. Creo que las clases auxiliares de Rails generalmente deben usarse para métodos de utilidad, no para casos de uso específicos, como el formato de atributos de modelo para cualquier tipo de lógica de presentación. Manténgalos ligeros y frescos.
  • Evite usar preocupaciones y use decoradores/delegadores en su lugar. ¿Por qué? Después de todo, las preocupaciones parecen ser una parte central de Rails y pueden SECAR el código cuando se comparten entre varios modelos. No obstante, el problema principal es que las preocupaciones no hacen que el objeto modelo sea más cohesivo. El código está mejor organizado. En otras palabras, no hay un cambio real en la API del modelo.
  • Intente extraer objetos de valor de los modelos para mantener su código limpio y agrupar atributos relacionados.
  • Pase siempre una variable de instancia por vista.

refactorización

Antes de comenzar, quiero discutir una cosa más. Cuando comienza la refactorización, generalmente termina preguntándose: "¿Es realmente buena la refactorización?"

Si siente que está separando o aislando más las responsabilidades (incluso si eso significa agregar más código y archivos nuevos), esto suele ser algo bueno. Después de todo, desacoplar una aplicación es una muy buena práctica y nos facilita realizar pruebas unitarias adecuadas.

No hablaré de cosas, como mover la lógica de los controladores a los modelos, ya que asumo que ya lo está haciendo y se siente cómodo usando Rails (generalmente el controlador delgado y el modelo FAT).

En aras de mantener este artículo ajustado, no discutiré las pruebas aquí, pero eso no significa que no debas probar.

Por el contrario, siempre debe comenzar con una prueba para asegurarse de que todo esté bien antes de seguir adelante. Esto es imprescindible, especialmente cuando se refactoriza.

Luego, podemos implementar cambios y asegurarnos de que todas las pruebas pasen para las partes relevantes del código.

Extracción de objetos de valor

Primero, ¿qué es un objeto de valor?

Martín Fowler explica:

Objeto de valor es un objeto pequeño, como dinero o un objeto de intervalo de fechas. Su propiedad clave es que siguen la semántica de valor en lugar de la semántica de referencia.

A veces te puedes encontrar con una situación en la que un concepto merece su propia abstracción y cuya igualdad no se basa en el valor, sino en la identidad. Los ejemplos incluirían la fecha, el URI y el nombre de la ruta de Ruby. La extracción a un objeto de valor (o modelo de dominio) es una gran conveniencia.

¿Por qué molestarse?

Una de las mayores ventajas de un objeto Value es la expresividad que ayudan a lograr en su código. Su código tenderá a ser mucho más claro, o al menos puede serlo si tiene buenas prácticas de nomenclatura. Dado que el objeto de valor es una abstracción, genera un código más limpio y menos errores.

Otra gran victoria es la inmutabilidad. La inmutabilidad de los objetos es muy importante. Cuando almacenamos ciertos conjuntos de datos, que podrían usarse en un objeto de valor, generalmente no quiero que se manipulen esos datos.

¿Cuándo es esto útil?

No hay una respuesta única y válida para todos. Haz lo que sea mejor para ti y lo que tenga sentido en cualquier situación dada.

Sin embargo, más allá de eso, hay algunas pautas que utilizo para ayudarme a tomar esa decisión.

Si piensas que un grupo de métodos está relacionado, con los objetos Value son más expresivos. Esta expresividad significa que un objeto de valor debe representar un conjunto distinto de datos, que su desarrollador promedio puede deducir simplemente mirando el nombre del objeto.

¿Cómo se hace esto?

Los objetos de valor deben seguir algunas reglas básicas:

  • Los objetos de valor deben tener múltiples atributos.
  • Los atributos deben ser inmutables durante todo el ciclo de vida del objeto.
  • La igualdad está determinada por los atributos del objeto.

En nuestro ejemplo, crearé un objeto de valor EntryStatus para abstraer los Entry#status_weather y Entry#status_landform a su propia clase, que se parece a esto:

  • muestra esencial

Nota: Esto es solo un objeto de Ruby antiguo simple (PORO) que no se hereda de ActiveRecord::Base . Hemos definido métodos de lectura para nuestros atributos y los estamos asignando en el momento de la inicialización. También usamos un mixin comparable para comparar objetos usando el método (<=>).

Podemos modificar el modelo de Entry para usar el objeto de valor que creamos:

  • muestra esencial

También podemos modificar el método EntryController#create para usar el nuevo objeto de valor en consecuencia:

  • muestra esencial

Extraer objetos de servicio

Entonces, ¿qué es un objeto de servicio?

El trabajo de un objeto de servicio es contener el código para una parte particular de la lógica empresarial. A diferencia del estilo de "modelo gordo" , donde una pequeña cantidad de objetos contiene muchos, muchos métodos para toda la lógica necesaria, el uso de objetos de servicio da como resultado muchas clases, cada una de las cuales tiene un solo propósito.

¿Por qué? ¿Cuales son los beneficios?

  • Desacoplamiento. Los objetos de servicio lo ayudan a lograr un mayor aislamiento entre los objetos.
  • Visibilidad. Los objetos de servicio (si están bien nombrados) muestran lo que hace una aplicación. Solo puedo echar un vistazo al directorio de servicios para ver qué capacidades proporciona una aplicación.
  • Limpieza de modelos y controladores. Los controladores convierten la solicitud (parámetros, sesión, cookies) en argumentos, los pasan al servicio y los redirigen o procesan de acuerdo con la respuesta del servicio. Mientras que los modelos solo se ocupan de las asociaciones y la persistencia. La extracción de código de controladores/modelos a objetos de servicio admitiría SRP y haría que el código estuviera más desacoplado. Entonces, la responsabilidad del modelo sería solo tratar con asociaciones y guardar/eliminar registros, mientras que el objeto de servicio tendría una sola responsabilidad (SRP). Esto conduce a un mejor diseño y mejores pruebas unitarias.
  • Seca y abraza el cambio. Mantengo los objetos de servicio tan simples y pequeños como puedo. Compongo objetos de servicio con otros objetos de servicio y los reutilizo.
  • Limpie y acelere su conjunto de pruebas. Los servicios son fáciles y rápidos de probar, ya que son pequeños objetos de Ruby con un punto de entrada (el método de llamada). Los servicios complejos se componen de otros servicios, por lo que puede dividir sus pruebas fácilmente. Además, el uso de objetos de servicio facilita la simulación/stub de objetos relacionados sin necesidad de cargar todo el entorno de Rails.
  • Llamable desde cualquier lugar. Es probable que los objetos de servicio se llamen desde los controladores, así como desde otros objetos de servicio, DelayedJob / Rescue / Sidekiq Jobs, tareas Rake, consola, etc.

Por otro lado, nada es perfecto. Una desventaja de los objetos de servicio es que pueden ser excesivos para una acción muy simple. En tales casos, es muy posible que termine complicando, en lugar de simplificar, su código.

¿Cuándo debe extraer objetos de servicio?

Aquí tampoco hay una regla dura y rápida.

Normalmente, los objetos de servicio son mejores para sistemas medianos y grandes; aquellos con una cantidad decente de lógica más allá de las operaciones CRUD estándar.

Entonces, cada vez que piense que un fragmento de código podría no pertenecer al directorio donde lo iba a agregar, probablemente sea una buena idea reconsiderarlo y ver si debería ir a un objeto de servicio en su lugar.

Aquí hay algunos indicadores de cuándo usar objetos de servicio:

  • La acción es compleja.
  • La acción se extiende a través de múltiples modelos.
  • La acción interactúa con un servicio externo.
  • La acción no es una preocupación central del modelo subyacente.
  • Hay múltiples formas de realizar la acción.

¿Cómo se deben diseñar los Objetos de Servicio?

Diseñar la clase para un objeto de servicio es relativamente sencillo, ya que no necesita gemas especiales, no tiene que aprender un nuevo DSL y puede confiar más o menos en las habilidades de diseño de software que ya posee.

Usualmente uso las siguientes pautas y convenciones para diseñar el objeto de servicio:

  • No almacene el estado del objeto.
  • Utilice métodos de instancia, no métodos de clase.
  • Debe haber muy pocos métodos públicos (preferiblemente uno para admitir SRP.
  • Los métodos deben devolver objetos de resultados enriquecidos y no valores booleanos.
  • Los servicios se encuentran en el directorio de app/services . Lo animo a usar subdirectorios para dominios de lógica de negocios pesados. Por ejemplo, el archivo app/services/report/generate_weekly.rb definirá Report::GenerateWeekly mientras que app/services/report/publish_monthly.rb definirá Report::PublishMonthly .
  • Los servicios comienzan con un verbo (y no terminan con Servicio): ApproveTransaction , SendTestNewsletter , ImportUsersFromCsv .
  • Los servicios responden al método de llamada. Descubrí que usar otro verbo lo hace un poco redundante: ApproveTransaction.approve() no se lee bien. Además, el método de llamada es el método de facto para lambda, procs y objetos de método.

Si observa StatisticsController#index , notará un grupo de métodos ( weeks_to_date_from , weeks_to_date_to , avg_distance , etc.) acoplados al controlador. Eso no es realmente bueno. Considere las ramificaciones si desea generar el informe semanal fuera de statistics_controller .

En nuestro caso, creemos Report::GenerateWeekly y extraigamos la lógica del informe de StatisticsController :

  • muestra esencial

Entonces StatisticsController#index ahora se ve más limpio:

  • muestra esencial

Al aplicar el patrón de objeto de servicio, agrupamos el código en torno a una acción específica y compleja y promovemos la creación de métodos más pequeños y claros.

Tarea: considere usar el objeto Value para WeeklyReport en lugar de Struct .

Extraer objetos de consulta de los controladores

¿Qué es un objeto de consulta?

Un objeto de consulta es un PORO que representa una consulta de base de datos. Se puede reutilizar en diferentes lugares de la aplicación y, al mismo tiempo, ocultar la lógica de consulta. También proporciona una buena unidad aislada para probar.

Debe extraer consultas SQL/NoSQL complejas en su propia clase.

Cada objeto de consulta es responsable de devolver un conjunto de resultados en función de los criterios/reglas comerciales.

En este ejemplo, no tenemos consultas complejas, por lo que usar el objeto Query no será eficiente. Sin embargo, para fines de demostración, extraigamos la consulta en Report::GenerateWeekly#call y creemos generate_entries_query.rb :

  • muestra esencial

Y en Report::GenerateWeekly#call , reemplacemos:

 def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end

con:

 def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end

El patrón de objeto de consulta ayuda a mantener la lógica de su modelo estrictamente relacionada con el comportamiento de una clase, al mismo tiempo que mantiene sus controladores delgados. Dado que no son más que simples clases antiguas de Ruby, los objetos de consulta no necesitan heredar de ActiveRecord::Base y deberían ser responsables de nada más que ejecutar consultas.

Extraer Crear entrada a un objeto de servicio

Ahora, extraigamos la lógica de crear una nueva entrada a un nuevo objeto de servicio. Usemos la convención y CreateEntry :

  • muestra esencial

Y ahora nuestro EntriesController#create es el siguiente:

 def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end

Mover validaciones a un objeto de formulario

Ahora, aquí las cosas empiezan a ponerse más interesantes.

Recuerde en nuestras pautas, acordamos que queríamos que los modelos contuvieran asociaciones y constantes, pero nada más (sin validaciones ni devoluciones de llamada). Entonces, comencemos eliminando las devoluciones de llamada y usemos un objeto de formulario en su lugar.

Un objeto Form es un objeto Plain Old Ruby (PORO). Toma el control del objeto de controlador/servicio donde sea que necesite hablar con la base de datos.

¿Por qué usar objetos de formulario?

Cuando busque refactorizar su aplicación, siempre es una buena idea tener en cuenta el principio de responsabilidad única (SRP).

SRP lo ayuda a tomar mejores decisiones de diseño sobre lo que debe ser responsable de una clase.

Su modelo de tabla de base de datos (un modelo ActiveRecord en el contexto de Rails), por ejemplo, representa un solo registro de base de datos en el código, por lo que no hay motivo para que se preocupe por nada de lo que esté haciendo su usuario.

Aquí es donde entran los objetos de formulario.

Un objeto de formulario es responsable de representar un formulario en su aplicación. Entonces, cada campo de entrada puede tratarse como un atributo en la clase. Puede validar que esos atributos cumplan con algunas reglas de validación y puede pasar los datos "limpios" a donde debe ir (por ejemplo, sus modelos de base de datos o quizás su generador de consultas de búsqueda).

¿Cuándo deberías usar un objeto Form?

  • Cuando desee extraer las validaciones de los modelos de Rails.
  • Cuando se pueden actualizar varios modelos mediante un solo envío de formulario, es posible que desee crear un objeto de formulario.

Esto le permite poner toda la lógica del formulario (convenciones de nomenclatura, validaciones, etc.) en un solo lugar.

¿Cómo se crea un objeto de formulario?

  • Cree una clase de Ruby simple.
  • Incluya ActiveModel::Model (en Rails 3, debe incluir Naming, Conversion y Validations en su lugar)
  • Comience a usar su nueva clase de formulario como si fuera un modelo regular de ActiveRecord, la mayor diferencia es que no puede conservar los datos almacenados en este objeto.

Tenga en cuenta que puede usar la gema de reforma, pero siguiendo con los PORO, crearemos entry_form.rb que se ve así:

  • muestra esencial

Y modificaremos CreateEntry para comenzar a usar el objeto de formulario EntryForm :

 class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end

Nota: algunos de ustedes dirían que no es necesario acceder al objeto Formulario desde el objeto Servicio y que podemos simplemente llamar al objeto Formulario directamente desde el controlador, que es un argumento válido. Sin embargo, preferiría tener un flujo claro, y es por eso que siempre llamo al objeto Formulario desde el objeto Servicio.

Mover devoluciones de llamada al objeto de servicio

Como acordamos anteriormente, no queremos que nuestros modelos contengan validaciones y devoluciones de llamadas. Extrajimos las validaciones utilizando objetos Form. Pero todavía estamos usando algunas devoluciones de llamada ( after_create en el modelo de Entry compare_speed_and_notify_user ).

¿Por qué queremos eliminar las devoluciones de llamada de los modelos?

Los desarrolladores de Rails generalmente comienzan a notar el dolor de devolución de llamada durante las pruebas. Si no está probando sus modelos de ActiveRecord, comenzará a notar problemas más adelante a medida que su aplicación crezca y se requiera más lógica para llamar o evitar la devolución de llamada.

Las devoluciones de llamada after_* se utilizan principalmente en relación con guardar o conservar el objeto.

Una vez que se guarda el objeto, se ha cumplido el propósito (es decir, la responsabilidad) del objeto. Por lo tanto, si aún vemos que se invocan devoluciones de llamada después de que se haya guardado el objeto, es probable que veamos devoluciones de llamada fuera del área de responsabilidad del objeto, y ahí es cuando nos encontramos con problemas.

En nuestro caso, estamos enviando un SMS al usuario después de guardar una entrada, que en realidad no está relacionada con el dominio de Entrada.

Una forma sencilla de resolver el problema es mover la devolución de llamada al objeto de servicio relacionado. Después de todo, el envío de un SMS para el usuario final está relacionado con el objeto de servicio CreateEntry y no con el modelo Entry en sí.

Al hacerlo, ya no tenemos que apagar el método compare_speed_and_notify_user en nuestras pruebas. Hemos simplificado la creación de una entrada sin necesidad de enviar un SMS y estamos siguiendo un buen diseño orientado a objetos al asegurarnos de que nuestras clases tengan una sola responsabilidad (SRP).

Así que ahora nuestro CreateEntry se parece a:

  • muestra esencial

Use decoradores en lugar de ayudantes

Si bien podemos usar fácilmente la colección Draper de modelos de vista y decoradores, me limitaré a los PORO por el bien de este artículo, como lo he estado haciendo hasta ahora.

Lo que necesito es una clase que llamará a métodos en el objeto decorado.

Puedo usar method_missing para implementar eso, pero usaré la biblioteca estándar SimpleDelegator de Ruby.

El siguiente código muestra cómo usar SimpleDelegator para implementar nuestro decorador base:

 % app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end

Entonces, ¿por qué el método _h ?

Este método actúa como un proxy para ver el contexto. De forma predeterminada, el contexto de vista es una instancia de una clase de vista, siendo la clase de vista predeterminada ActionView::Base . Puede acceder a los asistentes de visualización de la siguiente manera:

 _h.content_tag :div, 'my-div', class: 'my-class'

Para hacerlo más conveniente, agregamos un método de decorate a ApplicationHelper :

 module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end

Ahora, podemos mover los ayudantes de EntriesHelper a los decoradores:

 # app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end

Y podemos usar readable_time_period y readable_speed así:

 # app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
 - <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>

Estructura después de la refactorización

Terminamos con más archivos, pero eso no es necesariamente algo malo (y recuerde que, desde el principio, reconocimos que este ejemplo era solo para fines demostrativos y no era necesariamente un buen caso de uso para la refactorización):

 app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

Conclusión

Aunque nos enfocamos en Rails en esta publicación de blog, RoR no es una dependencia de los objetos de servicio descritos y otros PORO. Puede usar este enfoque con cualquier marco web, móvil o aplicación de consola.

Al usar MVC como la arquitectura de las aplicaciones web, todo permanece acoplado y lo hace ir más lento porque la mayoría de los cambios tienen un impacto en otras partes de la aplicación. Además, lo obliga a pensar dónde poner algo de lógica comercial: ¿debe ir al modelo, al controlador o a la vista?

Mediante el uso de PORO simples, hemos trasladado la lógica comercial a modelos o servicios que no se heredan de ActiveRecord , lo que ya es una gran victoria, sin mencionar que tenemos un código más limpio, que admite SRP y pruebas unitarias más rápidas.

La arquitectura limpia tiene como objetivo colocar los casos de uso en el centro/parte superior de su estructura, para que pueda ver fácilmente lo que hace su aplicación. También facilita la adopción de cambios ya que es mucho más modular y aislado.

Espero haber demostrado cómo el uso de Plain Old Ruby Objects y más abstracciones desacopla las preocupaciones, simplifica las pruebas y ayuda a producir un código limpio y fácil de mantener.

Relacionado: ¿Cuáles son los beneficios de Ruby on Rails? Después de dos décadas de programación, uso Rails