使用DI降低连接级别以提高代码可测试性,实现示例

在本文开头,我想立即指出,我并不假装是新手,而只是想分享/回想一下IoC DI这样的机会。


另外,我几乎没有写文章的经验,这是我的第一次。 如果您不严格判断,我会尽力而为。


我们在说什么


我遇到的大多数Rails项目都有一个大问题。 他们要么根本没有测试,要么他们的测试检查了一些无关紧要的部分,而这些测试的质量还有很多不足之处。


这样做的主要原因是,开发人员根本不知道如何编写代码,因此在单元测试中,仅测试由他编写的代码,而不测试例如包含在其他服务对象或库中的代码。


此外,在程序员的头上形成了逻辑链,但是为什么我什至还要把业务逻辑代码放在不同的层上,我在这里只添加几行,所有内容都将满足客户的需求。


这非常糟糕,因为单元测试失去了对重构的抵抗力,并且难以管理此类代码中的更改。 熵逐渐增加。 而且,如果您已经害怕重构代码,那就太糟糕了。


为了解决Java世界中的此类问题,许多库已经存在很长时间了,重新发明轮子没有多大意义,尽管应该指出的是,这些解决方案非常麻烦并且并非总是有使用它们的理由。 摩擦论者似乎以某种方式以不同的方式解决了此类问题,但老实说,我仍然不知道如何解决。 因此,我决定分享自己的决定方式。


在Ruby项目中如何解决这个问题的一般思路


基本思想是,对于具有依赖项的对象,我们应该能够对其进行管理。


考虑一个例子:


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 

为了测试block_user方法,我们陷入了不愉快的时刻,因为从NotificationService发出的通知将对我们有用,并且我们被迫处理该方法执行的最少部分。
例如,如果实现了UserService,那么Inversion使我们可以简单地摆脱这种情况:


 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 

现在,在测试时,我们将一个对象作为NotificationService模拟传递,并验证block_user以正确的顺序和正确的参数拉出notification_service方法。


 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 

轨道案例研究


当系统中有许多服务对象时,自己构造所有依赖项变得困难,代码开始增加多余的代码行,从而降低了可读性。


在这方面,我想到编写一个自动化依赖关系管理的小模块。


 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 

请注意,服务应为Singleton,即 有一个实例方法。 最简单的方法是在服务类中编写include Singleton


现在在ApplicationController中添加


 require 'services' class ApplicationController < ActionController::Base include Services::Injector end 

现在,在控制器中,我们可以执行此操作


 class WelcomeController < ApplicationController inject_service :welcome def index render plain: welcome_service.text end end 

在此控制器的规格中,我们自动获得instance_double(WelcomeService)作为依赖项。


 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 

有什么可以改善的


想象一下,例如,在我们的系统中,有多种方法可以发送通知,例如,晚上它将是一个提供者,而白天将是另一个提供者。 同时,提供商具有完全不同的发送协议。


通常,NotificationService接口保持不变,但是有两个特定的实现。


 class NightlyNotificationService < NotificationService end class DailyNotificationService < NotificationService end 

现在我们可以编写一个将执行条件映射服务的类


 class NotificationServiceMapper include Singleton def take now = Time.now ((now.hour >= 00) and (now.hour <= 8)) ? NightlyNotificationService : DailyNotificationService end end 

现在,当我们在Services :: Helpers :: Service.instance中获取服务实例时,我们需要检查是否存在* Mapper对象,如果存在,则将class常量传递给take。

Source: https://habr.com/ru/post/zh-CN422161/


All Articles