在本文开头,我想立即指出,我并不假装是新手,而只是想分享/回想一下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)
请注意,服务应为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。