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.