Al comienzo del art铆culo quiero se帽alar de inmediato que no pretendo ser nuevo, sino que solo quiero compartir / recordar una oportunidad como IoC DI.
Adem谩s, casi no tengo experiencia escribiendo art铆culos, este es el primero. Hice mi mejor esfuerzo, si no juzgas estrictamente.
De que estamos hablando
La mayor铆a de los proyectos de Rails con los que me he encontrado tienen un gran problema. O bien no tienen pruebas en absoluto, o sus pruebas verifican alguna parte insignificante, mientras que la calidad de estas pruebas deja mucho que desear.
La raz贸n principal de esto es que el desarrollador simplemente no sabe c贸mo escribir el c贸digo, por lo que en las pruebas unitarias solo prueba el c贸digo escrito por 茅l y no prueba el c贸digo, que, por ejemplo, est谩 contenido en alg煤n otro objeto de servicio o biblioteca.
Adem谩s, se forma una cadena l贸gica en la cabeza del programador, pero por qu茅 necesito transferir el c贸digo de l贸gica de negocios a otra capa, agregar茅 aqu铆 solo un par de l铆neas y todo cumplir谩 con los requisitos del cliente.
Y esto es muy malo, porque las pruebas unitarias pierden su resistencia a la refactorizaci贸n y la gesti贸n de los cambios en dicho c贸digo se vuelve dif铆cil. La entrop铆a est谩 aumentando gradualmente. Y si ya tiene miedo de refactorizar su c贸digo, las cosas est谩n muy mal.
Para resolver estos problemas en el mundo de Java, existen varias bibliotecas desde hace mucho tiempo y no hay una raz贸n particular para reinventar la rueda, aunque debe tenerse en cuenta que estas soluciones son muy engorrosas y no siempre hay una raz贸n para usarlas. Los rubististas parecen resolver de alguna manera tales problemas de una manera diferente, pero honestamente, todav铆a no entend铆a c贸mo. Por lo tanto, decid铆 compartir c贸mo decid铆 hacerlo.
La idea general de c贸mo resolver esto en proyectos ruby
La idea b谩sica es que para los objetos que tienen dependencias, deber铆amos poder administrarlos.
Considere un ejemplo:
class UserService def initialize() @notification_service = NotificationService.new end def block_user(user) user.block! @notification_service.send(user, 'you have been blocked') end end
Para probar el m茅todo block_user, nos encontramos en un momento desagradable, porque notificar desde NotificationService funcionar谩 para nosotros y nos vemos obligados a procesar una parte m铆nima que realiza este m茅todo.
La inversi贸n nos permite simplemente salir de esta situaci贸n si implementamos un servicio de usuario, por ejemplo, as铆:
class UserService def initialize(notification_service = NotificationService.new) @notification_service = notification_service end def block_user(user) user.block! @notification_service.send(user, 'you have been blocked') end end
Ahora, al probar, pasamos un objeto como simulaci贸n NotificationService, y verificamos que block_user extraiga los m茅todos de notify_service en el orden correcto y con los argumentos correctos.
RSpec.describe UserService, type: :service do let (:notification_service) { instance_double(NotificationService) } let (:service) { UserService.new(notification_service) } describe ".block_user" do let (:user) { instance_double(User) } it "should block user and send notification" do expect(user).to receive :block! expect(notification_service).to receive(:send).with(user, "you have been blocked") service.block_user(user) end end end
Estudio de caso para rieles
Cuando hay muchos objetos de servicio en el sistema, se hace dif铆cil construir todas las dependencias usted mismo, el c贸digo comienza a crecer con l铆neas de c贸digo adicionales que reducen la legibilidad.
En este sentido, se me ocurri贸 escribir un peque帽o m贸dulo que automatiza la gesti贸n de dependencias.
module Services module Injector def self.included(base)
Hay una advertencia, el servicio debe ser Singleton, es decir tener un m茅todo de instancia. La forma m谩s f谩cil de hacerlo es escribiendo include Singleton
en la clase de servicio.
Ahora en el ApplicationController agrega
require 'services' class ApplicationController < ActionController::Base include Services::Injector end
Y ahora en los controladores podemos hacer esto
class WelcomeController < ApplicationController inject_service :welcome def index render plain: welcome_service.text end end
En la especificaci贸n de este controlador, obtenemos autom谩ticamente instancia_doble (WelcomeService) como dependencia.
RSpec.describe WelcomeController, type: :controller do describe "index" do it "should render text from test service" do allow(controller.welcome_service).to receive(:text).and_return "OK" get :index expect(response).to have_attributes body: "OK" expect(response).to have_http_status 200 end end end
驴Qu茅 se puede mejorar?
Imagine, por ejemplo, que en nuestro sistema hay varias opciones sobre c贸mo podemos enviar notificaciones, por ejemplo, en la noche ser谩 un proveedor y otro durante el d铆a. Al mismo tiempo, los proveedores tienen protocolos de env铆o completamente diferentes.
En general, la interfaz NotificationService sigue siendo la misma, pero hay dos implementaciones espec铆ficas.
class NightlyNotificationService < NotificationService end class DailyNotificationService < NotificationService end
Ahora podemos escribir una clase que realizar谩 un mapeo condicional de servicios
class NotificationServiceMapper include Singleton def take now = Time.now ((now.hour >= 00) and (now.hour <= 8)) ? NightlyNotificationService : DailyNotificationService end end
Ahora, cuando tomamos la instancia de servicio en Services :: Helpers :: Service.instance, necesitamos verificar si hay un objeto * Mapper, y si es as铆, tomar la clase constante a trav茅s de take.