In the beginning of the article I want to immediately notice that I do not pretend to be new, but just want to share / recall such a possibility as IoC DI.
Also, I have almost no experience writing articles, this is my first. I tried as best I could, if you do not judge strictly.
Most of the Rails projects I've come across have one big problem. They either do not have tests at all, or their tests check some insignificant part, and the quality of these tests leaves much to be desired.
The main reason for this is that the developer simply does not know how to write code so that in unit tests only test the code written by him, and not test code that, say, is contained in some other service object or library.
Then a logical chain is formed in the programmer’s head, and why should I even carry the business logic code to another layer, I’ll add just a couple of lines and everything will meet the requirements of the customer.
And this is very bad, because unit testing is losing resistance to refactoring, it becomes difficult to manage changes in such code. Entropy is gradually increasing. And if you're already afraid of refactoring your code, things are very bad.
To solve such problems in the Java world, there have long been a number of libraries and there is not much point in reinventing the wheel, although it should be noted that these solutions are very cumbersome and there is not always a reason to use them. Rubisty apparently somehow solve similar problems, but I honestly did not understand how. By this, I decided to share how I decided to do it.
The basic idea is that for objects with dependencies, we must be able to manage them.
Consider an example:
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
To test the block_user method, we get into an unpleasant moment, because y will be triggered by a notify from the NotificationService and we are forced to process some minimal part that this method performs.
Inversion allows us to just get out of this situation if we implement the UserService, for example, like this:
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
Now when testing, we serve as a NotificationService mock object, and check that the block_user is jerking the notification_service methods in the correct order and with the right arguments.
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
When there are a lot of service objects in the system, it becomes difficult to construct all the dependencies yourself, the code begins to overgrow with unnecessary lines of code that reduce readability.
In this regard, it occurred to me to write a small module that automates dependency management.
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
There is one nuance, the service should be Singleton, i.e. have an instance method. The easiest way to do this is to write include Singleton
in the service class.
Now in ApplicationController add
require 'services' class ApplicationController < ActionController::Base include Services::Injector end
And now in the controllers we can do so
class WelcomeController < ApplicationController inject_service :welcome def index render plain: welcome_service.text end end
In the spec of this controller, we automatically get an instance_double (WelcomeService) as a dependency.
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
Imagine, for example, that in our system there are several options for how we can send notifications, for example, at night it will be one provider, and in the afternoon another. At the same time, providers have completely different sending protocols.
In general, the NotificationService interface remains the same, but there are two specific implementations.
class NightlyNotificationService < NotificationService end class DailyNotificationService < NotificationService end
Now we can write a class that will perform conditional mapping services
class NotificationServiceMapper include Singleton def take now = Time.now ((now.hour >= 00) and (now.hour <= 8)) ? NightlyNotificationService : DailyNotificationService end end
Now when we take the service instance in Services :: Helpers :: Service.instance, we need to check if there is a * Mapper object, and if there is, then take a class constant through take.
Source: https://habr.com/ru/post/422161/
All Articles