Classes de erro personalizadas Ruby: herança do atributo da mensagem

95

Não consigo encontrar muitas informações sobre classes de exceção personalizadas.

O que eu sei

Você pode declarar sua classe de erro personalizada e deixá-la herdar de StandardError, para que possa ser rescued:

class MyCustomError < StandardError
end

Isso permite que você aumente usando:

raise MyCustomError, "A message"

e mais tarde, receba essa mensagem ao resgatar

rescue MyCustomError => e
  puts e.message # => "A message"

O que eu não sei

Eu quero dar à minha exceção alguns campos personalizados, mas quero herdar o messageatributo da classe pai. Descobri lendo sobre este tópico que @messagenão é uma variável de instância da classe de exceção, então estou preocupado que minha herança não funcione.

Alguém pode me dar mais detalhes sobre isso? Como eu implementaria uma classe de erro personalizada com um objectatributo? O seguinte está correto:

class MyCustomError < StandardError
  attr_reader :object
  def initialize(message, object)
    super(message)
    @object = object
  end
end

E depois:

raise MyCustomError.new(anObject), "A message"

para obter:

rescue MyCustomError => e
  puts e.message # => "A message"
  puts e.object # => anObject

vai funcionar e, se funcionar, é esta a maneira correta de fazer as coisas?

MarioDS
fonte
3
Não rescue Exception => e. É mais amplo do que o padrão rescue => eque se estende de StandardErrore captura tudo, incluindo Ctrl + C. Eu faria rescue MyCustomError => e.
Ryan Taylor
1
@RyanTaylor Eu editei minha pergunta para uma abordagem mais adequada.
MarioDS de

Respostas:

121

raise já define a mensagem para que você não precise passá-la para o construtor:

class MyCustomError < StandardError
  attr_reader :object

  def initialize(object)
    @object = object
  end
end

begin
  raise MyCustomError.new("an object"), "a message"
rescue MyCustomError => e
  puts e.message # => "a message"
  puts e.object # => "an object"
end

Eu substituí rescue Exceptioncom rescue MyCustomError, veja Por que é um estilo ruim `resgatar Exception => e` em Ruby? .

Stefan
fonte
Vou aceitar sua resposta porque você me mostrou toda a sintaxe. Obrigado!
MarioDS de
1
Aqui estamos fazendo rescue Exception, mas por que não rescue MyCustomError?
Dfr
FYI, se o primeiro argumento, objeto, for uma opção e raise MyCustomError, "a message"sem new, "uma mensagem" não será definida.
hiroshi
Existe uma maneira de obter a mensagem gerada em nossa classe de exceção personalizada de alguma forma?
CyberMew
@CyberMew o que você quer dizer? O que você quer fazer?
Stefan
10

Dado o que a documentação do núcleo ruby Exception, da qual todos os outros erros herdam, afirma sobre#message

Retorna o resultado da chamada de exception.to_s. Normalmente, isso retorna a mensagem ou o nome da exceção. Ao fornecer um método to_str, as exceções concordam em serem usadas onde Strings são esperadas.

http://ruby-doc.org/core-1.9.3/Exception.html#method-i-message

Eu optaria por redefinir to_s/ to_strou o inicializador. Aqui está um exemplo em que queremos saber, de uma forma legível principalmente por humanos, quando um serviço externo falhou ao fazer algo.

NOTA: A segunda estratégia abaixo usa os métodos de string do Rails, como demodualize, que pode ser um pouco complicado e, portanto, potencialmente imprudente de fazer em uma exceção. Você também pode adicionar mais argumentos à assinatura do método, se necessário.

Substituindo #to_s Strategy e não #to_str, funciona de maneira diferente

module ExternalService

  class FailedCRUDError < ::StandardError
    def to_s
      'failed to crud with external service'
    end
  end

  class FailedToCreateError < FailedCRUDError; end
  class FailedToReadError < FailedCRUDError; end
  class FailedToUpdateError < FailedCRUDError; end
  class FailedToDeleteError < FailedCRUDError; end
end

Saída do console

begin; raise ExternalService::FailedToCreateError; rescue => e; e.message; end
# => "failed to crud with external service"

begin; raise ExternalService::FailedToCreateError, 'custom message'; rescue => e; e.message; end
# => "failed to crud with external service"

begin; raise ExternalService::FailedToCreateError.new('custom message'); rescue => e; e.message; end
# => "failed to crud with external service"

raise ExternalService::FailedToCreateError
# ExternalService::FailedToCreateError: failed to crud with external service

Substituindo #initialize Estratégia

Esta é a estratégia mais próxima das implementações que usei em rails. Como observado acima, que utiliza as demodualize, underscoree humanize ActiveSupportmétodos. Mas isso poderia ser facilmente removido, como na estratégia anterior.

module ExternalService
  class FailedCRUDError < ::StandardError
    def initialize(service_model=nil)
      super("#{self.class.name.demodulize.underscore.humanize} using #{service_model.class}")
    end
  end

  class FailedToCreateError < FailedCRUDError; end
  class FailedToReadError < FailedCRUDError; end
  class FailedToUpdateError < FailedCRUDError; end
  class FailedToDeleteError < FailedCRUDError; end
end

Saída do console

begin; raise ExternalService::FailedToCreateError; rescue => e; e.message; end
# => "Failed to create error using NilClass"

begin; raise ExternalService::FailedToCreateError, Object.new; rescue => e; e.message; end
# => "Failed to create error using Object"

begin; raise ExternalService::FailedToCreateError.new(Object.new); rescue => e; e.message; end
# => "Failed to create error using Object"

raise ExternalService::FailedCRUDError
# ExternalService::FailedCRUDError: Failed crud error using NilClass

raise ExternalService::FailedCRUDError.new(Object.new)
# RuntimeError: ExternalService::FailedCRUDError using Object

Ferramenta de demonstração

Esta é uma demonstração para mostrar o resgate e a transmissão de mensagens da implementação acima. A classe que levanta as exceções é uma API falsa para Cloudinary. Basta despejar uma das estratégias acima em seu console Rails, seguido por isso.

require 'rails' # only needed for second strategy 

module ExternalService
  class FailedCRUDError < ::StandardError
    def initialize(service_model=nil)
      @service_model = service_model
      super("#{self.class.name.demodulize.underscore.humanize} using #{@service_model.class}")
    end
  end

  class FailedToCreateError < FailedCRUDError; end
  class FailedToReadError < FailedCRUDError; end
  class FailedToUpdateError < FailedCRUDError; end
  class FailedToDeleteError < FailedCRUDError; end
end

# Stub service representing 3rd party cloud storage
class Cloudinary

  def initialize(*error_args)
    @error_args = error_args.flatten
  end

  def create_read_update_or_delete
    begin
      try_and_fail
    rescue ExternalService::FailedCRUDError => e
      e.message
    end
  end

  private def try_and_fail
    raise *@error_args
  end
end

errors_map = [
  # Without an arg
  ExternalService::FailedCRUDError,
  ExternalService::FailedToCreateError,
  ExternalService::FailedToReadError,
  ExternalService::FailedToUpdateError,
  ExternalService::FailedToDeleteError,
  # Instantiated without an arg
  ExternalService::FailedCRUDError.new,
  ExternalService::FailedToCreateError.new,
  ExternalService::FailedToReadError.new,
  ExternalService::FailedToUpdateError.new,
  ExternalService::FailedToDeleteError.new,
  # With an arg
  [ExternalService::FailedCRUDError, Object.new],
  [ExternalService::FailedToCreateError, Object.new],
  [ExternalService::FailedToReadError, Object.new],
  [ExternalService::FailedToUpdateError, Object.new],
  [ExternalService::FailedToDeleteError, Object.new],
  # Instantiated with an arg
  ExternalService::FailedCRUDError.new(Object.new),
  ExternalService::FailedToCreateError.new(Object.new),
  ExternalService::FailedToReadError.new(Object.new),
  ExternalService::FailedToUpdateError.new(Object.new),
  ExternalService::FailedToDeleteError.new(Object.new),
].inject({}) do |errors, args|
  begin 
    errors.merge!( args => Cloudinary.new(args).create_read_update_or_delete)
  rescue => e
    binding.pry
  end
end

if defined?(pp) || require('pp')
  pp errors_map
else
  errors_map.each{ |set| puts set.inspect }
end
Chad M
fonte
6

Sua ideia está certa, mas a forma como você a chama está errada. Deveria ser

raise MyCustomError.new(an_object, "A message")
Sawa
fonte
Ok, pensei que a mensagem que você deu era um segundo parâmetro para a raisepalavra - chave ou algo assim.
MarioDS de
Você redefiniu initializepara tomar dois argumentos. newpassa os argumentos para initialize.
sawa
Ou você pode omitir os parênteses.
sawa de
Eu entendo que pouco, mas o cartaz do tema I ligada a na minha pergunta não é assim: raise(BillRowError.new(:roamingcalls, @index), "Roaming Calls field missing"). Então ele chama raisecom dois parâmetros: um novo BillRowErrorobjeto e sua mensagem. Estou apenas confuso com a sintaxe ... Em outros tutoriais eu sempre vejo assim:raise Error, message
MarioDS
1
O problema não é para quantos argumentos você passa raise; isso é muito flexível. O problema é que você definiu initializelevar dois argumentos e deu apenas um. Veja no seu exemplo. BillRowError.new(:roamingcalls, @index)recebe dois argumentos.
sawa de
4

Eu queria fazer algo semelhante. Eu queria passar um objeto para #new e ter a mensagem definida com base em algum processamento do objeto passado. O seguinte funciona.

class FooError < StandardError
  attr_accessor :message # this is critical!
  def initialize(stuff)
    @message = stuff.reverse
  end
end

begin
  raise FooError.new("!dlroW olleH")
rescue FooError => e
  puts e.message #=> Hello World!
end

Observe que, se você não declarar attr_accessor :message, não funcionará. Ao resolver o problema do OP, você também pode passar a mensagem como um argumento adicional e armazenar o que quiser. A parte crucial parece substituir #message.

Huliax
fonte