Objetos de servicio con rieles usando Aldous

Uno de los conceptos con los que hemos tenido un gran éxito en el equipo de Tuts + son los objetos de servicio. Hemos utilizado objetos de servicio para reducir el acoplamiento en nuestros sistemas, hacerlos más verificables y hacer que la lógica de negocios importante sea más obvia para todos los desarrolladores del equipo.. 

Entonces, cuando decidimos codificar algunos de los conceptos que hemos usado en nuestro desarrollo de Rails en una gema Ruby (llamada Aldous), los objetos de servicio fueron los primeros en la lista.

Lo que me gustaría hacer hoy es dar un rápido resumen de los objetos de servicio como los hemos implementado en Aldous. Esperamos que esto le indique la mayoría de las cosas que necesita saber para utilizar los objetos de servicio de Aldous en sus propios proyectos..

La anatomía de un objeto de servicio básico

Foto por Dennis Skley

Un objeto de servicio es básicamente un método que está envuelto en un objeto. A veces, un objeto de servicio puede contener varios métodos, pero la versión más simple es solo una clase con un método, por ejemplo:

clase DoSomething def perform # do stuff end end end

Todos estamos acostumbrados a usar sustantivos para nombrar nuestros objetos, pero a veces puede ser difícil encontrar un buen sustantivo para representar un concepto, mientras que hablar de él en términos de una acción (o verbo) es simple y natural. Un objeto de servicio es lo que obtenemos cuando 'vamos con el flujo' y simplemente convertimos el verbo en un objeto.

Por supuesto, dada la definición anterior, podemos convertir cualquier acción / método en un objeto de servicio si así lo deseamos. El seguimiento…

clase Cliente def createPurchase (order) # do stuff end end end

... podría convertirse en:

clase CreateCustomerPurchase def initialize (customer, order) end def ejecuta # do stuff end end end

Podríamos escribir varias otras publicaciones sobre el efecto que podrían tener los objetos de servicio en el diseño de su sistema, las diversas concesiones que realizará, etc. Por ahora, simplemente seamos conscientes de ellos como un concepto y los consideremos simplemente otra herramienta tenemos en nuestro arsenal.

Por qué usar objetos de servicio en rieles

A medida que las aplicaciones de Rails aumentan de tamaño, nuestros modelos tienden a ser bastante grandes, por lo que buscamos formas de llevar algunas funcionalidades a los objetos "auxiliares". Pero esto es a menudo más fácil decirlo que hacerlo. Rails no tiene un concepto, en la capa de modelo, que es más granular que un modelo. Así que terminas teniendo que hacer muchas llamadas de juicio:

  • ¿Creas un modelo PORO o creas una clase en el lib carpeta?
  • ¿Qué métodos te mueves en esta clase?
  • ¿Cómo nombrar sensiblemente esta clase dado los métodos que hemos movido en ella?? 

Ahora debe comunicar lo que ha hecho a los otros desarrolladores de su equipo y a las personas nuevas que se unan más adelante. Y, por supuesto, ante una situación similar, otros desarrolladores pueden hacer diferentes juicios de juicio, lo que lleva a incoherencias que se arrastran en.

Los objetos de servicio nos dan un concepto que es más granular que un modelo. Podemos tener una ubicación coherente para todos nuestros servicios y solo puede mover un método a un servicio. Nombra esta clase después de la acción / método que representará. Podemos extraer la funcionalidad en objetos más granulares sin demasiadas llamadas de criterio, lo que mantiene a todo el equipo en la misma página, lo que nos permite continuar con el negocio de crear una gran aplicación. 

El uso de objetos de servicio reduce el acoplamiento entre sus modelos de Rails, y los servicios resultantes son altamente reutilizables debido a su tamaño pequeño / espacio ligero. 

Los objetos de servicio también son altamente comprobables, ya que por lo general no requieren tanta repetición de prueba como los objetos más pesados, y solo te preocupa probar el método que contiene el objeto.. 

Tanto los objetos de servicio como sus pruebas son fáciles de leer / entender ya que son altamente cohesivos (también un efecto secundario de su pequeño tamaño). También puede descartar y reescribir ambos objetos de servicio y sus pruebas casi a voluntad, ya que el costo de hacerlo es relativamente bajo y es muy fácil mantener su interfaz.

Los objetos de servicio definitivamente tienen mucho a su favor, especialmente cuando los introduces en tus aplicaciones Rails. 

Objetos de servicio con Aldous

Foto por Trevor Leyenhorst

Dado que los objetos de servicio son tan simples, ¿por qué incluso necesitamos una gema? ¿Por qué no solo crear POROs, y entonces no necesita preocuparse por otra dependencia?? 

Definitivamente podrías hacer eso, y de hecho lo hicimos durante bastante tiempo en Tuts +, pero a través del uso extensivo terminamos desarrollando algunos patrones de servicios que hicieron nuestra vida un poco más fácil, y esto es exactamente lo que hemos hecho. empujado en Aldous. Estos patrones son ligeros y no implican mucha magia. Hacen nuestras vidas un poco más fáciles, pero conservamos todo el control si lo necesitamos.

Donde deben vivir

Lo primero es lo primero, ¿dónde deberían vivir sus servicios? Solemos ponerlos en aplicación / servicios, por lo que necesita lo siguiente en su app / config / application.rb:

config.autoload_paths + =% W (# config.root / app / services) config.eager_load_paths + =% W (# config.root / app / services)

Lo que deberían ser llamados

Como mencioné anteriormente, tendemos a nombrar objetos de servicio después de acciones / verbos (por ejemplo,. Crear usuario, Reembolso de compra), pero también tendemos a añadir "servicio" a todos los nombres de clase (por ejemplo,. CreateUserService, ReembolsoComprarServicio). De esta manera, sin importar en qué contexto esté (mirando los archivos en el sistema de archivos, mirando una clase de servicio en cualquier parte del código), siempre sabe que está tratando con un objeto de servicio..

La gema no impone el cumplimiento de ninguna manera, pero vale la pena tenerla en cuenta como una lección aprendida..

Los objetos de servicio son inmutables

Cuando decimos inmutable, queremos decir que después de que se inicializa el objeto, su estado interno ya no cambiará. Esto es realmente genial, ya que hace que sea mucho más sencillo razonar acerca del estado de cada objeto, así como del sistema en general..

Para que lo anterior sea cierto, el método de objeto de servicio no puede cambiar el estado del objeto, por lo que cualquier dato debe devolverse como una salida del método. Esto es difícil de hacer cumplir directamente, ya que un objeto siempre tendrá acceso a su propio estado interno. Con Aldous intentamos imponerlo a través de la convención y la educación, y las siguientes dos secciones le mostrarán cómo.

Representando el éxito y el fracaso

Un objeto de servicio Aldous siempre debe devolver uno de los dos tipos de objetos:

  • Aldous :: Servicio :: Resultado :: Éxito
  • Aldous :: Servicio :: Resultado :: Falla

Aquí hay un ejemplo:

clase CreateUserService < Aldous::Service def perform user = User.new(user_data_hash) if user.save Result::Success.new else Result::Failure.new end end end

Porque heredamos de Aldous :: Servicio, Podemos construir nuestros objetos de retorno como Resultado :: Éxito. Usar esos objetos como valores de retorno nos permite hacer cosas como:

hash =  result = CreateUserService.perform (hash) si result.success? # hacer cosas de lo contrario # resultado. # terminan las cosas del fracaso

Podríamos, en teoría, devolver verdadero o falso y obtener el mismo comportamiento que hemos descrito anteriormente, pero si lo hiciéramos, no podríamos cargar ningún dato adicional con nuestro valor de retorno, ya menudo queremos llevar datos..

Usando DTOs

El éxito o el fracaso de una operación / servicio es solo una parte de la historia. A menudo, hemos creado algún objeto que queremos devolver, o hemos producido algunos errores de los cuales queremos notificar el código de llamada. Es por eso que devolver objetos, como hemos mostrado anteriormente, es útil. Estos objetos no solo se utilizan para indicar el éxito o el fracaso, también son objetos de transferencia de datos.

Aldous le permite anular un método en la clase de servicio base, para especificar un conjunto de valores predeterminados que contendrían los objetos devueltos por el servicio, por ejemplo:

clase CreateUserService < Aldous::Service attr_reader :user_data_hash def initialize(user_data_hash) @user_data_hash = user_data_hash end def default_result_data user: nil end def perform user = User.new(user_data_hash) if user.save Result::Success.new(user: user) else Result::Failure.new end end end

Las claves hash contenidas en default_result_data se convertirá automáticamente en métodos en el Resultado :: Éxito y Resultado :: Falla Objetos devueltos por el servicio. Y si proporciona un valor diferente para una de las claves en ese método, anulará el valor predeterminado. Así que en el caso de la clase anterior:

hash =  result = CreateUserService.perform (hash) si result.success? result.user # será una instancia de User result.blah # generaría un error else # result.failure? result.user # será nulo result.blah # provocaría un final de error

En efecto las teclas hash en el default_result_data Método son un contrato para los usuarios del objeto de servicio. Le garantizamos que podrá llamar a cualquier clave en ese hash como método en cualquier objeto de resultado que salga del servicio.

API sin errores

Imagen de Roberto Zingales.

Cuando hablamos de API sin errores nos referimos a métodos que nunca generan errores, sino que siempre devuelven un valor para indicar el éxito o el fracaso. He escrito sobre API sin errores antes. Los servicios de Aldous están libres de errores dependiendo de cómo los llame. En el ejemplo anterior: 

result = CreateUserService.perform (hash)

Esto nunca generará un error. Internamente Aldous envuelve su método de ejecución en un rescate bloque y si su código genera un error devolverá un Resultado :: Falla con el default_result_data como datos. 

Esto es bastante liberador, porque ya no tiene que pensar en lo que puede salir mal con el código que ha escrito. Solo está interesado en el éxito o el fracaso de su servicio, y cualquier error resultará en un error. 

Esto es genial para la mayoría de las situaciones. Pero a veces, quieres que se genere un error. El mejor ejemplo de esto es cuando está utilizando un objeto de servicio en un trabajador en segundo plano y un error podría hacer que el trabajador en segundo plano vuelva a intentarlo. Es por esto que un servicio de Aldous también recibe mágicamente un realizar! método y le permite anular otro método de la clase base. Aquí está nuestro ejemplo otra vez:

clase CreateUserService < Aldous::Service attr_reader :user_data_hash def initialize(user_data_hash) @user_data_hash = user_data_hash end def raisable_error MyApplication::Errors::UserError end def default_result_data user: nil end def perform user = User.new(user_data_hash) if user.save Result::Success.new(user: user) else Result::Failure.new end end end

Como puede ver, ahora hemos anulado la raisable_error método. A veces queremos que se produzca un error, pero tampoco queremos que se trate de ningún tipo de error. De lo contrario, nuestro código de llamada tendría que tomar conciencia de todos los posibles errores que el servicio puede producir, o ser forzado a detectar uno de los tipos de error básicos. Es por eso que cuando usas el realizar! método, Aldous seguirá detectando todos los errores por usted, pero luego volverá a aumentar la raisable_error Ha especificado y establecido el error original como la causa. Ahora puedes tener esto:

hash =  begin service = CreateUserService.build (hash) result = service.perform! rescate service.raisable_error => e # error cosas final

Probando objetos de servicio de Aldous

Es posible que haya notado el uso del método de fábrica:

CreateUserService.build (hash) CreateUserService.perform (hash)

Siempre debe usar estos, y nunca construir objetos de servicio directamente. Los métodos de fábrica son los que nos permiten enganchar limpiamente las características agradables como el rescate automático y la adición de default_result_data.

Sin embargo, cuando se trata de pruebas, no debe preocuparse por cómo Aldous aumenta la funcionalidad de sus objetos de servicio. Entonces, al probar, simplemente construya los objetos directamente usando el constructor y luego pruebe su funcionalidad. Obtendrá especificaciones para la lógica que escribió y confiará en que Aldous hará lo que se supone que debe hacer (Aldous tiene sus propias pruebas para esto) cuando se trata de producción..

Conclusión

Esperamos que esto te haya dado una idea de cómo los objetos de servicio (y especialmente los objetos de servicio de Aldous) pueden ser una buena herramienta en tu arsenal cuando trabajas con Ruby / Rails. Prueba Aldous y haznos saber lo que piensas. También siéntase libre de echar un vistazo al código de Aldous. No solo lo escribimos para que sea útil, sino también para que sea legible y fácil de entender / modificar..