web-dev-qa-db-fra.com

Ruby classes d'erreur personnalisées: héritage de l'attribut de message

Je n'arrive pas à trouver beaucoup d'informations sur les classes d'exceptions personnalisées.

Ce que je sais

Vous pouvez déclarer votre classe d'erreur personnalisée et la laisser hériter de StandardError, de sorte qu'elle puisse être rescued:

class MyCustomError < StandardError
end

Cela vous permet de l'élever en utilisant:

raise MyCustomError, "A message"

et plus tard, recevez ce message lors du sauvetage

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

Ce que je ne sais pas

Je souhaite attribuer à mon exception des champs personnalisés, mais je souhaite hériter de l'attribut message de la classe parente. J'ai découvert la lecture sur ce sujet que @message n'est pas une variable d'instance de la classe d'exception, je crains donc que mon héritage ne fonctionne pas.

Quelqu'un peut-il me donner plus de détails à ce sujet? Comment implémenter une classe d'erreur personnalisée avec un attribut object? Est-ce que ce qui suit est correct:

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

Et alors:

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

obtenir:

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

cela fonctionnera-t-il, et si cela fonctionne, est-ce la bonne façon de faire les choses?

90
MarioDS

raise définit déjà le message afin que vous n'ayez pas à le transmettre au constructeur:

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

J'ai remplacé rescue Exception avec rescue MyCustomError, voir Pourquoi est-ce un mauvais style de `rescue Exception => e` en Ruby? .

119
Stefan

Compte tenu de ce que la Ruby documentation de base de Exception, dont héritent toutes les autres erreurs, énonce à propos de #message

Renvoie le résultat de l'appel de exception.to_s. Normalement, cela retourne le message ou le nom de l’exception. En fournissant une méthode to_str, les exceptions acceptent d'être utilisées lorsque des chaînes sont attendues.

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

Je choisirais de redéfinir to_s/to_str ou l'initialiseur. Voici un exemple où nous voulons savoir, de manière essentiellement lisible par l'homme, lorsqu'un service externe n'a pas réussi à faire quelque chose.

NOTE: La deuxième stratégie ci-dessous utilise les jolies méthodes de chaîne Rails, telles que demodualize, ce qui peut être un peu compliqué et donc potentiellement peu judicieux dans une exception. Vous pouvez également ajoutez plus d'arguments à la signature de la méthode, si vous en avez besoin.

Remplacement de la stratégie #to_s pas #to_str, cela fonctionne différemment

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

Sortie de la 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

Remplacement de la stratégie #initialize

C'est la stratégie la plus proche des implémentations que j'ai utilisées dans Rails. Comme indiqué ci-dessus, il utilise les méthodes demodualize, underscore et humanizeActiveSupport. Mais cela pourrait être facilement supprimé, comme dans la stratégie précédente.

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

Sortie de la 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

Outil de démonstration

Ceci est une démonstration pour montrer le sauvetage et la messagerie de la mise en œuvre ci-dessus. La classe soulevant les exceptions est une fausse API pour Cloudinary. Il suffit de déposer l’une des stratégies ci-dessus dans votre Rails console, suivie de celle-ci).

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
8
Chad M

Votre idée est bonne, mais la façon dont vous l'appelez est fausse. CA devrait etre

raise MyCustomError.new(an_object, "A message")
6
sawa

Je voulais faire quelque chose de similaire. Je voulais transmettre un objet à #new et définir l'ensemble des messages en fonction du traitement de l'objet transmis. Les travaux suivants.

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

Notez que si vous ne déclarez pas attr_accessor :message alors cela ne fonctionnera pas. En abordant le problème du PO, vous pouvez également passer le message comme argument supplémentaire et stocker tout ce que vous voulez. La partie cruciale semble être primordiale # message.

2
Huliax