Un problema habitual al trabajar con Rails es decidir dónde colocar la lógica de nuestras funcionalidades.
La lógica a menudo se coloca en los Controladores, Modelos, o si tenemos suerte en un Objeto de Servicio. Entonces, si tenemos Objetos de Servicio, ¿por qué necesitamos Casos de Uso?
Sígueme en este artículo para descubrir los beneficios de este patrón.
Caso práctico
Definición
Un caso de uso es una lista de acciones o pasos de eventos que suelen definir las interacciones entre un rol y un sistema para alcanzar un objetivo.
Vale la pena mencionar que este patrón se aplica de muchas maneras diferentes y tiene nombres alternativos. Podemos encontrarlo como Interactores, Operadores o Comandospero en el Ruby comunidad con la que nos quedamos Caso práctico. Cada implementación es diferente, pero con el mismo propósito: servir a un caso de uso del sistema por parte del usuario.
Aunque en nuestro proyecto no estamos definiendo los requisitos utilizando Caso prácticos y UML este patrón sigue siendo útil para estructurar la lógica empresarial de forma práctica.
Reglas
Nuestra Casos prácticos debe ser:
Marco agnóstico
Base de datos agnóstica
Responsable de una sola cosa (definir los pasos para alcanzar el objetivo del usuario)
Beneficios
Legibilidad: Fácil de leer y entender ya que los pasos están claramente definidos.
Desacoplamiento: Desplazar la lógica de Controladores y Modelos y crear un nuevo nivel de abstracción.
Visibilidad: El código base revela las funciones disponibles en el sistema.
A la práctica
Tomemos el ejemplo de un usuario que quiere comprar algo en nuestro sistema.
módulo UseCases
módulo Buyer
clase Compra
def initialize(comprador:, carrito:)
@comprador = comprador
@carrito = carrito
end
def call
return unless comprobar_stock
return unless crear_compra
notify end
privado
attr_reader :comprador, :carrito
def comprobar_stock
Services::CheckStock.call(carrito: carrito)
end
def crear_compra
Servicios::CrearCompra.call(comprador: comprador, carrito: carrito).call
end
def notificar
Servicios::NotificarComprador.call(comprador: comprador)
end
end
end
fin
Como puede ver en este código creamos un nuevo Caso práctico llamado Compra. Definimos sólo un método público llame a. Dentro del método call, encontramos pasos bastante básicos para realizar una compra, y todos los pasos están definidos como métodos privados. Cada paso está llamando a un Objeto de Servicio, de esta forma nuestro Caso práctico es sólo definir los pasos para realizar una compra y no la lógica en sí. Esto nos da una idea clara de lo que se puede hacer en nuestro sistema (realizar una compra) y los pasos para conseguirlo.
Ahora estamos listos para llamar a nuestro primer Caso práctico de un controlador.
clase Controlador
def compra
UseCases::Comprador::Compra.new(
comprador: parámetros_compra[:comprador],
carrito: parámetros_de_compra[:carrito]
).call
...
fin
...
fin
Desde esta perspectiva, la Caso práctico se parece bastante a un Objeto de Servicio pero el propósito es diferente. Un Objeto de Servicio realiza una tarea de bajo nivel e interactúa con diferentes partes del sistema como la Base de Datos mientras que el Objeto de Servicio El caso de uso crea una nueva abstracción de alto nivel y define los pasos lógicos.
Mejoras
Nuestro primer Caso práctico funciona pero podría ser mejor. ¿Cómo podríamos mejorarlo? Utilicemos el seco gemas. En este caso vamos a utilizar transacción en seco.
Primero definamos nuestra clase base.
clase UseCase
include Dry::Transacción
class << self
def llamada(**cargas)
new.call(**args)
end
end
end
Esto nos ayudará a pasar atributos a la transacción UseCase y utilizarlos. Entonces estamos listos para redefinir nuestro Caso de Uso de Compra.
módulo UseCases
módulo Buyer
clase Compra
def initialize(comprador:, carrito:)
@comprador = comprador
@carrito = carrito
end
def call
return unless comprobar_stock
return unless crear_compra
notificar
end
privado
attr_reader :comprador, :carrito
def comprobar_stock
Services::CheckStock.call(carrito: carrito)
end
def crear_compra
Servicios::CrearCompra.call(comprador: comprador, carrito: carrito).call
end
def notificar
Servicios::NotificarComprador.call(comprador: comprador)
end
end
end
end
Con los nuevos cambios, podemos ver de forma clara como se definen nuestros pasos y podemos gestionar el resultado de cada paso con Success() y Failure().
Estamos listos para llamar a nuestro nuevo Caso de Uso en el controlador y preparar nuestra respuesta dependiendo del resultado final.
clase Controlador
def compra
UseCases::Comprador::Compra.new.call(
comprador: parámetros_compra[:comprador],
carrito: parámetros_de_compra[:carrito]
) do |result|
result.success do
...
end
resultado.fallo do
...
end
fin
...
fin
...
fin
Este ejemplo podría mejorarse aún más con validaciones, pero es suficiente para mostrar la potencia de este patrón.
Conclusiones
Seamos sinceros, el Patrón de casos de uso es bastante simple y se parece mucho a un Objeto de Servicio, pero este nivel de abstracción puede suponer un gran cambio en tu aplicación.
Imagina que un nuevo desarrollador se une al proyecto y abre la carpeta use_cases, como primera impresión tendrá una lista de todas las funcionalidades disponibles en el sistema y tras abrir un Caso de Uso verá todos los pasos necesarios para esa funcionalidad sin profundizar en la lógica. Esta sensación de orden y control es el mayor beneficio de este patrón.
Llévese esto en su caja de herramientas y quizá en el futuro haga buen uso de él.