Javier Valencia Javier Valencia
Código fuente en la pantalla de un editor

Rails sostenible (III): la lógica de negocio no va en los Active Records

Javier Valencia · · 5 min de lectura · 2 visitas · Desarrollo
rails ruby rails-sostenible arquitectura service-objects diseño

Tercera entrega de la serie Rails sostenible, sobre el libro de David Bryant Copeland. Tiempo de lectura estimado: 14 minutos.

Si esta serie tuviera que reducirse a un solo post, sería este. Toda la propuesta de Copeland gira alrededor de una frase que da título a su capítulo cinco y que reaparece, ampliada, en el capítulo quince: la lógica de negocio no va en los Active Records. Vimos en la primera entrega que Rails no le da casa a la lógica de negocio. Hoy se la construimos.

Qué es la lógica de negocio (y por qué es peligrosa)

Copeland define la lógica de negocio como lo que hace especial a tu aplicación. No es el CRUD —eso lo hace cualquier framework—, sino las reglas concretas de tu dominio: cómo se calcula el precio de un pedido con descuentos y cupones, qué pasa cuando un usuario cancela una suscripción, cuándo se considera que un envío está "retrasado". Es el código que no podrías copiar de otro proyecto porque es tuyo.

Y precisamente por eso es peligroso, por tres razones que el libro desgrana:

  1. Es un imán de complejidad. Las reglas de negocio se acumulan, se contradicen, tienen excepciones y casos límite. Es, de lejos, la parte más enrevesada del sistema.
  2. Sufre mucho churn. Cambia constantemente, porque el negocio cambia constantemente. Lo que hoy es "10% de descuento a partir de 50 €" mañana es otra cosa.
  3. Un bug aquí tiene efectos amplios. Si la lógica vive en clases muy referenciadas, un error se propaga por todo el sistema.

Logo de Ruby

El problema de meterla en los Active Records

Junta esas tres características con la realidad de un Active Record y verás el desastre. El modelo Order lo usa medio sistema: los controladores, las vistas, los jobs, los mailers, los tests. Es una clase crítica y omnipresente. Si además le metes dentro toda la lógica de cálculo de precios, validaciones contextuales y disparo de side effects, conviertes una clase ya frágil por su uso en una clase que además cambia cada semana.

# El anti-patrón: el "fat model" que lo sabe todo
class Order < ApplicationRecord
  belongs_to :customer
  has_many :line_items

  after_create :send_confirmation_email
  after_create :notify_warehouse
  after_update :recalculate_loyalty_points

  def total
    subtotal = line_items.sum(&:price)
    subtotal -= discount_for(customer)
    subtotal += shipping_cost
    subtotal * (1 + tax_rate)
  end

  def discount_for(customer)
    # 40 líneas de reglas de cupones, fidelidad, promociones...
  end
  # ...y otras 600 líneas
end

Esta clase mezcla tres responsabilidades que cambian por motivos distintos: el acceso a datos (las asociaciones), las reglas de negocio (cálculo de precios) y los efectos colaterales (emails, almacén). Cada una tiene su propio ritmo de cambio. Tenerlas pegadas significa que tocar una te obliga a entender y arriesgar las otras. Eso es lo contrario de sostenible.

El seam: una costura entre Rails y tu dominio

Aquí Copeland introduce el concepto que vertebra su solución: el seam (costura). Un seam es un punto del sistema donde puedes separar dos mundos: el mundo de Rails (HTTP, base de datos, vistas) y el mundo de tu lógica de negocio. La idea es que los controladores, jobs y demás clases frontera deleguen la lógica de negocio a clases que no saben nada de Rails.

Tu lógica de negocio debería poder leerse y entenderse sin necesidad de saber que existe un controlador, un request HTTP o una tabla en Postgres.

Esa frontera —el seam— es lo que te permite que cada lado evolucione a su ritmo. Los controladores cambian cuando cambia la interfaz; la lógica de negocio cambia cuando cambian las reglas; y la una no arrastra a la otra.

Servicios stateless con nombres explícitos

¿Cómo se materializa el seam? Con una capa de servicios. Pero ojo, Copeland es muy específico sobre cómo deben ser estos servicios, porque "service object" en la comunidad Rails significa muchas cosas y no todas buenas:

  • Stateless: el servicio no guarda estado entre llamadas. No tiene atributos mutables que representen "el progreso" de algo. Recibe lo que necesita, hace su trabajo, devuelve un resultado.
  • Nombres explícitos de clase: nada de un cajón de sastre OrderService que acumula 30 métodos inconexos. Mejor clases con nombre propio que describan una operación del dominio.
  • Nombres explícitos de método: el método dice qué hace en lenguaje del negocio.

Copeland propone agrupar estos servicios en una clase de dominio que actúa como punto de entrada. Por ejemplo, un objeto Customers con métodos como create_customer o cancel_subscription:

# app/services/customers.rb
class Customers
  def create_customer(email:, name:)
    customer = Customer.create!(email: email, name: name)
    Welcome.deliver_later(customer)   # side effect explícito
    Result.new(created: true, customer: customer)
  end
end

Y el controlador, que vive del lado de Rails del seam, se limita a traducir el request y delegar:

class CustomersController < ApplicationController
  def create
    result = Customers.new.create_customer(
      email: params[:email],
      name: params[:name],
    )
    if result.created?
      redirect_to customer_path(result.customer)
    else
      render :new, status: :unprocessable_entity
    end
  end
end

Fíjate en lo que ha pasado: el controlador no sabe cómo se crea un cliente, solo que existe una operación de dominio llamada "crear cliente". El servicio no sabe que existe un controlador. La costura está limpia. Y el Customer (el Active Record) ha vuelto a su sitio: ser una pasarela hacia la base de datos, no el cerebro del negocio.

Patrones que conviene evitar

El capítulo quince dedica buena parte a desaconsejar implementaciones populares de "service object" que, según Copeland, suelen empeorar las cosas:

  • El servicio con estado y un único call. Ese patrón de MyService.new(args).call donde el objeto guarda los argumentos como atributos introduce estado mutable innecesario y oscurece qué hace cada método. Copeland prefiere métodos explícitos sobre objetos stateless.
  • Abusar de ApplicationService con magia compartida. Las clases base que inyectan comportamiento mágico (success, failure, callbacks) tienen su propio carrying cost: hay que entender la base para entender cualquier servicio.
  • Confundir "un servicio por acción de controlador" con diseño. Tener un CreateOrderService, UpdateOrderService, DeleteOrderService que reflejan el CRUD no aporta nada: estás renombrando el controlador. Los servicios deben modelar operaciones del dominio, no acciones HTTP.

La regla de fondo: el código de negocio debe revelar comportamiento. Al leer la clase, debes entender qué hace el negocio, no perderte en andamiaje genérico.

Mi versión

Esta es la idea del libro que más he peleado en proyectos reales, y la que más resistencia genera, porque va contra el "Rails way" que mucha gente interioriza como dogma. El argumento que mejor me funciona no es teórico, es práctico: enseño el modelo User de 900 líneas y pregunto quién se atreve a tocarlo sin sudar. Nadie. Ese miedo es el coste de la insostenibilidad, hecho carne.

Dicho esto, también he visto el extremo opuesto: equipos que, mal interpretando el consejo, montan una capa de servicios con 200 clases XxxService de un solo método que son indistinguibles de funciones sueltas y que, encima, todas heredan de una base mágica imposible de seguir. Eso también es insostenible. La clave que Copeland repite y que yo suscribo: servicios stateless, con nombres del dominio, sin magia. Si tu capa de servicios no se lee como un glosario de tu negocio, algo va mal.

Mi heurística personal: el Active Record puede tener métodos de consulta y de presentación de datos propios (un full_name, un scope sencillo), pero en cuanto aparece un side effect, una regla con varias ramas o una operación que coordina varios objetos, eso se va al seam. El modelo guarda y lee; el servicio decide.

Lo que viene

Hemos construido la pieza central: un seam que separa Rails de tu dominio, con servicios stateless de nombres explícitos. A partir de aquí, el libro recorre cada capa de Rails aplicando esta filosofía. En el próximo post empezamos por la puerta de entrada de toda petición: las rutas y las plantillas HTML —rutas canónicas, recursos en vez de acciones custom, HTML semántico, partials como componentes reutilizables y por qué Copeland defiende seguir usando ERB sin complejos.