用Rails拯救普通Ruby对象的异常DSL

95 阅读2分钟

在某些时候,你可以开始考虑在你的Rails应用程序的纯Ruby对象中拥有与Rails的控制器相同的DSL,以便在rescue_from 的帮助下拯救来自行动的异常,这将是一件好事。如果你对这个话题感兴趣,请继续阅读,我将展示如何从头开始实现它。

**TL;DR:**使用ActiveSupport::Rescuable

简介

在任何Rails控制器中捕捉所有的目标异常,这并不是什么秘密。只要在一个基本的控制器中使用rescue_from ,像这样。

class ApplicationController < ActionController::Base
  rescue_from CanCan::AccessDenied do
    redirect_to root_path, alert: "You don't have access to this page."
  end
end

就可以了。每当CanCan::AccessDenied ,从ApplicationController 继承控制器的一些动作中被引发,Ruby解释器就会跳转到这个块,用rescue_from 定义,然后就执行。结果是,用户将被重定向到主页面,并发出警报,原来的异常被抑制了。有不同的方法来使用rescue_from 方法,但这篇文章不是关于这个的,所以你可以在这里找到所有的变化。

这篇文章的主要想法是告诉你如何在你的Rails应用程序的一些其他类型的对象中添加这种功能。比如说,你有一堆服务对象,它们位于同一个层次结构的分支中,你想在这些服务对象的基类中轻松捕获一些一般的异常,你认为在子类中拥有这种功能会很好。

用Rescuable扩展自定义对象

实际上,这根本就不是问题。你所要做的只是在基类中包含ActiveSupport::Rescuable模块,并将负责执行某些工作的方法包裹起来,这些工作可能会引发异常,你想在以后用rescue_from

为了不啰嗦,我只提供下面的代码片段来演示这个主要的想法。

class BaseService
  include ActiveSupport::Rescuable

  class FieldIsNilError < StandardError; end

  rescue_from FieldIsNilError do |exception|
    puts "Field is empty: #{exception.class} - #{exception.message}"
  end

  def call
    call_with_rescue { useful_yield }
  end

  private

  def call_with_rescue
    yield
  rescue => e
    rescue_with_handler(e) || raise(e)
  end

  def useful_yield
    fail NotImplementedError
  end
end

class LoginUserService < BaseService
  class InvalidEmailError < StandardError; end
  class EmptyEmailError < StandardError; end
  class SecurityError < StandardError; end

  rescue_from InvalidEmailError, EmptyEmailError do |exception|
    puts "Logged invalid login attempt: #{exception.class} - #{exception.message}"
  end

  def initialize(email: nil)
    @email = email
  end

  private

  def useful_yield
    case @email
    when 'invalid'
      fail InvalidEmailError, 'email is invalid'
    when ''
      fail EmptyEmailError, 'email is empty'
    when 'kill -9'
      fail SecurityError, 'throw out'
    when nil
      fail FieldIsNilError, 'email is nil'
    else
      puts 'login ok'
    end
  end
end

LoginUserService.new(email: 'invalid').call
# => Logged invalid login attempt: LoginUserService::InvalidEmailError - email is invalid
LoginUserService.new(email: '').call
# => Logged invalid login attempt: LoginUserService::EmptyEmailError - email is empty
LoginUserService.new(email: 'ok@email.com').call
# => login ok
LoginUserService.new(email: nil).call
# => Field is empty: BaseService::FieldIsNilError - email is nil
LoginUserService.new(email: 'kill -9').call
# => throw out (LoginUserService::SecurityError)

这里的主要技巧是在include ActiveSupport::Rescuable 。它为我们提供了rescue_from方法,定义在类的层面上。它还添加了rescue_with_handler 方法,该方法试图为一个被提出的异常找到一个处理程序,如果找到了就调用这个处理程序。处理程序与rescue_from 一起定义在一个服务对象中--它只是一个块。我们在call_with_rescue 方法中使用这个方法,这个方法包装了那个做真正工作的方法,并且在某个时候可以引发一个异常(这是useful_yield 方法)。而这个异常可以用rescue_from 来捕获,并且可以做一些有用的工作来压制这个错误。或者在我们没有为这个异常定义救援处理程序的情况下,它将被提出来,由终端用户来观察它。

现在让我们来实验一下这段代码。只需将上述代码放在位于Rails应用程序中的test.rb 文件中,并使用以下命令在rails runner中执行它。rails runner test.rb.你会有一个类似于这个的输出。

Logged invalid login attempt: LoginUserService::InvalidEmailError - email is invalid
Logged invalid login attempt: LoginUserService::EmptyEmailError - email is empty
login ok
Field is empty: BaseService::FieldIsNilError - email is nil
test.rb:49:in `useful_yield': throw out (LoginUserService::SecurityError)

如果你在这一点上感到不舒服,你可以修改这段代码,然后用rails runner重新运行它,正如你所看到的,这相当简单。或者直接在这个帖子上发表评论。我很高兴听到你的回应和问题。

结论

尝试了解有趣的东西是如何实现的,你希望在你的代码中拥有这些东西,并消费这些实现。但是请注意,有时自己从头开始实现一些东西会更容易,这可能有很多原因:代码质量,缺乏不容易扩展的功能等等。每个案例都应该被分析,并做出正确的决定。但这不是这个案例的问题。ActiveSupport::Rescuable 做它的工作,而且做得很优雅。

用Rails拯救普通Ruby对象的异常DSL最初由Andrei Kaleshka于2017年1月19日在WideFix发表。