Reducci贸n del nivel de conectividad utilizando DI para mejorar la capacidad de prueba del c贸digo, ejemplo de implementaci贸n

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) # TODO: check base, should be controller or service base.extend ClassMethods end module ClassMethods def inject_service(name) service = Services::Helpers::Service.new(name) attr_writer service.to_s define_method service.to_s do instance_variable_get("@#{service.to_s}").tap { |s| return s if s } instance_variable_set("@#{service.to_s}", service.instance) end end end end module Helpers class Service def initialize(name) raise ArgumentError, 'name of service should be a Symbol' unless name.is_a? Symbol @name = name.to_s.downcase @class = "#{@name.camelcase}Service".constantize unless @class.respond_to? :instance raise ArgumentError, "#{@name.to_s} should be singleton (respond to instance method)" end end def to_s "#{@name}_service" end def instance if Rails.env.test? if defined? RSpec::Mocks::ExampleMethods extend RSpec::Mocks::ExampleMethods instance_double @class else nil end else @class.instance end end end end end 

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.

Source: https://habr.com/ru/post/es422161/


All Articles