Creación de Service Object para Gestión de Estados de Tareas en Rails

Clase 30 de 34Curso Intermedio de Ruby on Rails

Contenido del curso

Pruebas

Resumen

Separar la lógica de cambio de estado de una tarea en un Service Object dedicado es una práctica que prepara tu aplicación para crecer sin acumular complejidad dentro de los modelos. Aquí se construye paso a paso el servicio Task::TriggerEvent, desde la prueba hasta la implementación, aplicando metaprogramación de Ruby y respetando el flujo TDD.

¿Por qué crear un Service Object intermedio para cambiar estados?

Cuando componentes externos de Rails necesitan modificar el estado de una tarea, acceder directamente al modelo genera acoplamiento. Un Service Object actúa como barrera intermedia que expone una interfaz clara y controlada. Aunque la lógica inicial parezca mínima, este patrón habilita el escalamiento futuro: políticas de seguridad, políticas de acceso, inyección de dependencias con otros servicios y conexiones adicionales pueden incorporarse sin tocar el modelo.

  • Facilita la fragmentación de responsabilidades.
  • Prepara el sistema para recibir security policies y validaciones extra.
  • Permite inyectar dependencias con otros servicios de forma ordenada.

¿Cómo escribir la prueba del servicio TriggerEvent?

Siguiendo TDD, lo primero es crear el archivo de especificación. Dentro de spec/services/task/, se duplica el archivo send_email_spec.rb y se renombra a trigger_event_spec.rb [01:15].

¿Qué elementos necesita la prueba?

El servicio recibe dos parámetros: la referencia a la tarea y el nombre del evento que se quiere disparar. En la prueba se define una variable let(:event) con el valor start [02:30]. El subject se construye con described_class.new pasando task y event.

Dentro del contexto with a valid task, se persiste la tarea y luego se evalúan tres expectativas:

  • Que success sea verdadero.
  • Que task.status sea igual a inprocess después de disparar el evento start [03:18].
  • Que el conteo de transiciones (task.transitions.count) sea igual a 1, porque cada cambio de estado debe generar un registro de transición [03:45].

¿Cómo se estructura el servicio TriggerEvent?

El archivo se crea en app/services/task/ duplicando send_email.rb y renombrándolo a trigger_event.rb [04:10]. El nombre de la clase debe coincidir exactamente con el que aparece en la cabecera de la prueba: Task::TriggerEvent.

El servicio define dos campos de entrada: task y event, cumpliendo con la llamada uniforme que se espera de todo Service Object en el proyecto.

¿Cómo funciona la metaprogramación en el lanzamiento del evento?

La parte central del método call utiliza metaprogramación de Ruby para invocar dinámicamente el evento sobre la tarea [05:02]. En lugar de escribir manualmente cada transición posible, se construye la llamada así:

ruby task.send("#{event}!")

Si el valor de event es start, esta línea equivale a:

ruby task.start!

El bang (!) al final se agrega mediante interpolación de cadena. Esta técnica permite que un único servicio maneje cualquier evento definido en la máquina de estados sin necesidad de condicionales adicionales. Ruby permite enviar mensajes dinámicos a objetos mediante send, y eso es precisamente lo que se aprovecha aquí.

¿Por qué algo tan simple justifica un servicio?

Aunque el cuerpo del método parece trivial, el servicio se convierte en el punto de entrada oficial para toda transición de estado [05:45]. En un escenario real se podrían agregar:

  • Políticas de seguridad que validen permisos del usuario.
  • Políticas de acceso que restrinjan qué roles pueden disparar ciertos eventos.
  • Conexiones con otros servicios que ejecuten acciones colaterales como notificaciones o auditoría.

Sin el Service Object, toda esa lógica terminaría contaminando el modelo o los controladores.

Finalmente, la ejecución de la prueba se realiza desde la consola con el comando rspec [06:30]. El resultado muestra Task::TriggerEvent#call pasando correctamente, confirmando que el servicio dispara el evento, actualiza el estado y registra la transición.

¿Te animas a crear un Service Object similar para generar el código de la tarea después de su creación? Comparte tu implementación y veamos cómo lo resuelves.