Clases de error personalizadas de Ruby: herencia del atributo de mensaje

95

Parece que no puedo encontrar mucha información sobre las clases de excepción personalizadas.

Lo que yo se

Puede declarar su clase de error personalizada y dejar que herede StandardError, por lo que puede ser rescued:

class MyCustomError < StandardError
end

Esto te permite aumentarlo usando:

raise MyCustomError, "A message"

y luego, recibe ese mensaje al rescatar

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

Lo que no se

Quiero darle a mi excepción algunos campos personalizados, pero quiero heredar el messageatributo de la clase principal. Descubrí al leer sobre este tema que @messageno es una variable de instancia de la clase de excepción, por lo que me preocupa que mi herencia no funcione.

¿Alguien puede darme más detalles sobre esto? ¿Cómo implementaría una clase de error personalizada con un objectatributo? ¿Es correcto lo siguiente:

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

Y entonces:

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

Llegar:

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

¿Funcionará y, si lo hace, es esta la forma correcta de hacer las cosas?

MarioDS
fuente
3
No lo hagas rescue Exception => e. Es más amplio que el predeterminado rescue => eque se extiende desde StandardErrory captura todo, incluido Ctrl + C. Yo haría rescue MyCustomError => e.
Ryan Taylor
1
@RyanTaylor Edité mi pregunta para un enfoque más adecuado.
MarioDS

Respuestas:

121

raise ya establece el mensaje para que no tenga que pasarlo al constructor:

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

Lo reemplacé rescue Exceptioncon rescue MyCustomError, vea ¿Por qué es un mal estilo para `rescatar Excepción => e` en Ruby? .

Stefan
fuente
Aceptaré tu respuesta porque me mostraste la sintaxis completa. ¡Gracias!
MarioDS
1
Aquí lo hacemos rescue Exception, pero ¿por qué no rescue MyCustomError?
Dfr
Para su información, si el primer argumento, objeto, es una opción y raise MyCustomError, "a message"sin new"un mensaje" no se establecerá.
hiroshi
¿Hay alguna forma de obtener el mensaje generado en nuestra clase de excepción personalizada de alguna manera?
CyberMew
@CyberMew ¿a qué te refieres? ¿Qué quieres hacer?
Stefan
10

Dado lo que la documentación básica de ruby ​​de Exception, de la que heredan todos los demás errores, establece sobre#message

Devuelve el resultado de invocar exception.to_s. Normalmente, esto devuelve el mensaje o el nombre de la excepción. Al proporcionar un método to_str, se aceptan excepciones para usarse donde se esperan cadenas.

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

Optaría por redefinir to_s/ to_stro el inicializador. Aquí hay un ejemplo en el que queremos saber, de una manera legible principalmente por humanos, cuando un servicio externo no ha podido hacer algo.

NOTA: La segunda estrategia a continuación utiliza los métodos de cadena de rieles bonitos, como demodualize, que puede ser un poco complicado y, por lo tanto, potencialmente imprudente en una excepción. También puede agregar más argumentos a la firma del método, si lo necesita.

Anular la estrategia #to_s no #to_str, funciona de manera 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

Salida de consola

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

Anulación de la estrategia #initialize

Esta es la estrategia más cercana a las implementaciones que he usado en rieles. Como se señaló anteriormente, utiliza los demodualize, underscorey humanize ActiveSupportmétodos. Pero esto podría eliminarse fácilmente, como en la estrategia 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

Salida de consola

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

Herramienta de demostración

Esta es una demostración para mostrar el rescate y la mensajería de la implementación anterior. La clase que genera las excepciones es una API falsa para Cloudinary. Simplemente descargue una de las estrategias anteriores en su consola de rieles, seguida de esto.

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
fuente
6

Tu idea es correcta, pero la forma en que la llamas es incorrecta. Debería ser

raise MyCustomError.new(an_object, "A message")
sawa
fuente
De acuerdo, pensé que el mensaje que diste era un segundo parámetro para la raisepalabra clave o algo.
MarioDS
Lo redefinió initializepara tomar dos argumentos. newpasa los argumentos a initialize.
sawa
O puede omitir los paréntesis.
sawa
Entiendo que poco, pero el cartel del tema he vinculado en mi pregunta hace como este: raise(BillRowError.new(:roamingcalls, @index), "Roaming Calls field missing"). Entonces llama raisecon dos parámetros: un nuevo BillRowErrorobjeto y su mensaje. Estoy confundido por la sintaxis ... En otros tutoriales siempre lo veo así:raise Error, message
MarioDS
1
El problema no es a cuántos argumentos le pasa raise; eso es bastante flexible. El problema es que definió initializetomar dos argumentos y solo dio uno. Mira tu ejemplo. BillRowError.new(:roamingcalls, @index)se le dan dos argumentos.
sawa
4

Quería hacer algo similar. Quería pasar un objeto a #new y tener el mensaje establecido en función de algún procesamiento del objeto pasado. Los siguientes trabajos.

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

Tenga en cuenta que si no declara attr_accessor :message, no funcionará. Al abordar el problema del OP, también puede pasar el mensaje como un argumento adicional y almacenar lo que quiera. La parte crucial parece ser #mensaje primordial.

Huliax
fuente