arrow_back Volver
Inicio keyboard_arrow_right Artículos keyboard_arrow_right Artículo

Patrón Service object en Ruby on Rails

Eduardo Ismael Garcia

Full Stack Developer at Código Facilito.

av_timer 4 Min. de lectura

remove_red_eye 1613 visitas

calendar_today 19 Febrero 2024

Mantener nuestro código legible y bien estructurado es una de las principales tareas, y quizás una de las más complicadas, que debemos tener presentes al momento de desarrollar software.

Un código legible y bien estructurado se traduce en código fácil de leer, testear y, sobre todo, fácil de mantener.

Es por ello que el uso de buenas practicas de desarrollo tales como seguir un estándar de codificación o implementar patrones de diseño, siempre serán útiles al momento de crear software de calidad.

Con esto en mente, el día de hoy me gustaría hablemos de un patron de diseño que podemos usar en nuestros proyectos con Ruby on Rails, me refiero al patrón Sevice Object. Hablaremos de cómo funciona y en que casos es bueno, o no, utilizarlo.

Si eras una persona apasionada a Rails, o estas comenzando a dar tus primeros pasos con este Framework sin duda este entrega te resultará de mucha utilidad.

Bien, sin más introducción comencemos con esta nueva entrega.

Service object

Entremos de lleno con el tema. Service Object es un patrón de diseño muy popular en la comunidad de Rails. Es común mente usado para encapsular y reutilizar código. Resulta particularmente útil cuando la lógica de negocios que deseamos implementar es compleja y requiere de múltiples pasos y validaciones que, a su vez, requieren de otros componentes del proyecto (Modelos, Controladores, Helpers, Tasks etc…).

Mediante este patron evitamos que modelos y/o controladores implementen lógica de negocio que no les corresponde; obteniendo así un proyecto mucho mejor estructurado y limpio.

Veamos un ejemplo para que nos quede más en claro.

Imaginemos que nos encontramos trabajando en una tienda en línea donde debemos implementar el proceso de checkout, es decir, el proceso cuando un usuario desea completar su compra.

Para este proceso puede constar de los siguientes pasos:

1.- Validar la orden.

2.- Completar y confirmar el proceso de pago.

3.- Crear registro de pago exitoso.

4.- Enviar correo electrónico de confirmación de compra.

5.- Actualizar stock de los productos adquiridos.

Una vez con los requerimientos listos ya podemos implementar el proceso de checkout.

Es muy común que en primera instancia intentemos realizar todo el proceso desde el controlador. Aquí un ejemplo del cómo puede quedar nuestro código.

class OrdersController < ApplicationController
  
	def create
    @order = Order.new(order_params)

    validate_order

    validate_process_payment
    send_confirmation_email
    update_inventory

    session[:cart] = nil 
    flash[:success] = "Order successfully placed!"
  end

	def validate_order
    ...
  end

  def valida_process_payment
    ...
  end

	def send_confirmation_email 
    OrderMailer.with(order: @order).confirmation_email.deliver_later
  end
  
  def update_inventory
    @order.products.each do |product|
	     product.update(...)
    end
  end

end

En este ejemplo usamos el método create para validar y llamar a otros métodos involucrados en el proceso.

Si bien es cierto el código puede funcionar, la verdad es que estamos rompiendo varios patrones de diseño, y, sobre todo, no respetando un par de principios SOLID.

Para mejorar esto podemos implementar lo que la comunidad de Rails hace mucho eco: “Skinny Controller, Fat Model”, intentando mover nuestra lógica de negocios del controlador al modelo.

Aquí el ejemplo del modelo.

class Order < ActiveRecord::Base
  
	def self.check_out!(order_params)
    order = Order.new(order_params)

    validate_order(order)
    validate_process_payment(order)
    send_confirmation_email(order)
    update_inventory(order)
  end
	
	def self.validate_order(order)
    ...
  end

  def self.valida_process_payment(order)
    ...
  end

	def self.send_confirmation_email(order)
    OrderMailer.with(order: order).confirmation_email.deliver_later
  end
  
  def self.update_inventory(order)
    order.products.each do |product|
	     product.update(...)
    end
  end
  
end

Si bien es cierto este approach tiene más sentido que dejar todo en el controlador, también tiene sus problemas, ya que lógica de negocios compleja, con múltiples pasos y validaciones, puede resultar en modelos sumamente grandes, con cientos o miles de líneas de código. Que, paradójicamente, resultaría en un anti patrón de diseño.

Para solventar este problema podemos implementar el patrón de diseño service object. Este patrón, cómo mencionamos al principio, permite encapsular lógica de negocios compleja que dependa de múltiples componentes de Rails en un solo lugar.

Usando este patrón nuestro código queda de la siguiente manera.

# app/services/checkout_service.rb

class CheckoutService
  
	def initialize(params)
    @params = params
  end

  def call
		order = Order.new(params) 

    validate_order(order)
    validate_process_payment(order)
    send_confirmation_email(order)
    update_inventory(order)

		true
  rescue => e
    false
  end

  private 
 
  def validate_order(order)
    ...
  end

  def valida_process_payment(order)
    ...
  end

	def send_confirmation_email(order)
    OrderMailer.with(order: order).confirmation_email.deliver_later
  end
  
  def update_inventory(order)
    order.products.each do |product|
	     product.update(...)
    end
  end
end

Y nuestro controlador el código quedaría así:


class OrdersController < ApplicationController
  def create
    checkout_service = CheckoutService.new(order_params)

    if checkout_service.call
      session[:cart] = nil 
      redirect_to root_path, notice: "Order successfully placed!"
    else
      redirect_to cart_path, alert: "Checkout failed. Please try again."
    end
  end

end

Con nuestro service delegamos toda la lógica de negocios para el proceso de Checkout a la clase CheckoutService. Todos los métodos y atributos de esta clase serán única, y exclusivamente, para el proceso de checkout. Esto permite que los modelos y controladores no posean lógica de negocios innecesarias.

Para este refactor, ahora nuestro controlador se enfoca únicamente en recibir y responder a las peticiones del cliente. Ya no este componente quien valida e implementa los pasos del checkout.

Si el día de mañana tenemos que modificar el proceso de checkout, simplemente nos dirigimos al servicios, hacemos los cambios y no deberíamos afectar otros componentes de Rails cómo lo pudiera ser el controlador o los modelos.

De igual forma, si el día de mañana demos implementar un proceso diferente de Checkout para ciertos tipos de usuarios, solo debemos crear un nuevo Service y listo, dejamos a un lado las condiciones para discernir que debemos, o no, ejecutar.

La estructura que se propone para los servicio es simple. El nombre del servicio debe ser descriptivo y siempre usando el sufijo Service.

De igual forma en la clase se recomienda, por lo menos, 2 métodos. El método initialize y el método call. Si bien estos no son obligatorios, es estandar que se usa hoy en día, así que te recomiendo hagas uso de ellos.

Conclusión

Siempre que tengamos lógica compleja que implementar, y esta haga uso de otros componentes de Rails (Controladores, Modelos, Helpers, Tasks etc…), lo recomendable es siempre abstraer y encapsular esta lógica, del forma que nuestros proyectos se encuentre muchos mejor estructurados. Delegando responsabilidades a los componentes correctos.

El uso del patrón Service object nos viene muy bien para lograr todo esto. Lo servicios se vuelven fáciles de leer, testear y mantener.

Bootcamp Ciencia de Datos

12 semanas de formación intensiva en los conocimientos, fundamentos, y herramientas que necesitas para ser científico de datos

Más información