No começo do artigo, quero observar imediatamente que não pretendo ser novo, mas quero compartilhar / relembrar uma oportunidade como a IoC DI.
Além disso, quase não tenho experiência em escrever artigos, este é o meu primeiro. Eu tentei o meu melhor, se você não julga estritamente.
Do que estamos falando?
A maioria dos projetos Rails que encontrei tem um grande problema. Eles não têm testes, ou seus testes verificam parte insignificante, enquanto a qualidade desses testes deixa muito a desejar.
A principal razão para isso é que o desenvolvedor simplesmente não sabe escrever o código, de modo que, nos testes de unidade, teste apenas o código escrito por ele e não o código, que, por exemplo, está contido em outro objeto ou biblioteca de serviço.
Além disso, uma cadeia lógica é formada na cabeça do programador, mas por que eu deveria colocar o código de lógica de negócios em uma camada diferente, adicionarei apenas algumas linhas aqui e tudo atenderá aos requisitos do cliente.
E isso é muito ruim, porque o teste de unidade perde sua resistência à refatoração e o gerenciamento de alterações nesse código se torna difícil. A entropia está aumentando gradualmente. E se você já tem medo de refatorar seu código, as coisas estão muito ruins.
Para resolver esses problemas no mundo Java, existem várias bibliotecas há muito tempo e faz pouco sentido reinventar a roda, embora deva-se notar que essas soluções são muito volumosas e nem sempre há um motivo para usá-las. Os rubististas parecem resolver de algum modo esses problemas de uma maneira diferente, mas honestamente, eu ainda não entendi como. Portanto, decidi compartilhar como decidi fazê-lo.
A idéia geral de como resolver isso em projetos ruby
A idéia básica é que, para objetos que possuem dependências, devemos poder gerenciá-los.
Considere um exemplo:
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 testar o método block_user, chegamos a um momento desagradável, porque a notificação do NotificationService funcionará para nós e somos forçados a processar uma parte mínima que esse método executa.
A inversão nos permite simplesmente sair dessa situação se implementarmos um UserService, por exemplo, assim:
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
Agora, ao testar, passamos um objeto como simulação NotificationService e verificamos se o block_user puxa os métodos notification_service na ordem correta e com os argumentos corretos.
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
Estudo de caso para trilhos
Quando há muitos objetos de serviço no sistema, torna-se difícil construir todas as dependências, o código começa a crescer com linhas extras de código que reduzem a legibilidade.
Nesse sentido, ocorreu-me escrever um pequeno módulo que automatiza o gerenciamento de dependências.
module Services module Injector def self.included(base)
Há uma ressalva, o serviço deve ser Singleton, ou seja, tem um método de instância. A maneira mais fácil de fazer isso é escrevendo include Singleton
na classe de serviço.
Agora, no ApplicationController, adicione
require 'services' class ApplicationController < ActionController::Base include Services::Injector end
E agora nos controladores podemos fazer isso
class WelcomeController < ApplicationController inject_service :welcome def index render plain: welcome_service.text end end
Nas especificações deste controlador, obtemos automaticamente instance_double (WelcomeService) como uma dependência.
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
O que pode ser melhorado
Imagine, por exemplo, que em nosso sistema existem várias opções de como podemos enviar notificações, por exemplo, à noite, será um provedor e outro durante o dia. Ao mesmo tempo, os provedores têm protocolos de envio completamente diferentes.
Em geral, a interface NotificationService permanece a mesma, mas há duas implementações específicas.
class NightlyNotificationService < NotificationService end class DailyNotificationService < NotificationService end
Agora podemos escrever uma classe que executará o mapeamento condicional dos serviços
class NotificationServiceMapper include Singleton def take now = Time.now ((now.hour >= 00) and (now.hour <= 8)) ? NightlyNotificationService : DailyNotificationService end end
Agora, quando pegamos a instância de serviço em Services :: Helpers :: Service.instance, precisamos verificar se há um objeto * Mapper e, se houver, então, leve a classe constante através de take.