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

95

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, afin qu'elle puisse être rescued:

class MyCustomError < StandardError
end

Cela vous permet de l'augmenter 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 veux donner à mon exception des champs personnalisés, mais je veux hériter de l' messageattribut de la classe parent. J'ai découvert la lecture sur ce sujet qui @messagen'est pas une variable d'instance de la classe d'exception, alors je crains que mon héritage ne fonctionnera pas.

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

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

Puis:

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

obtenir:

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

fonctionnera-t-il, et si c'est le cas, est-ce la bonne façon de faire les choses?

MarioDS
la source
3
Ne fais pas ça rescue Exception => e. Il est plus large que la valeur par défaut rescue => equi s'étend de StandardErroret capture tout, y compris Ctrl + C. Je ferais rescue MyCustomError => e.
Ryan Taylor
1
@RyanTaylor J'ai édité ma question pour l'approche la plus appropriée.
MarioDS

Réponses:

121

raise définit déjà le message afin que vous n'ayez pas à le passer 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 Exceptionpar rescue MyCustomError, voir Pourquoi est-ce un mauvais style de `sauver Exception => e` dans Ruby? .

Stefan
la source
J'accepte votre réponse car vous m'avez montré toute la syntaxe. Merci!
MarioDS
1
Ici, nous faisons rescue Exception, mais pourquoi pas rescue MyCustomError?
Dfr
Pour info, si le premier argument, objet, est une option et raise MyCustomError, "a message"sans new, "un message" ne sera pas défini.
hiroshi
Existe-t-il un moyen d'obtenir le message déclenché dans notre classe d'exception personnalisée?
CyberMew
@CyberMew que voulez-vous dire? Qu'est-ce que tu veux faire?
Stefan
10

Étant donné ce que la documentation de base de ruby Exception, dont toutes les autres erreurs héritent, indique à propos de#message

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

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

J'opterais pour la redéfinition to_s/ to_strou l'initialiseur. Voici un exemple où nous voulons savoir, d'une manière principalement lisible par l'homme, quand un service externe n'a pas réussi à faire quelque chose.

REMARQUE: La deuxième stratégie ci-dessous utilise les méthodes de jolies chaînes de rails, telles que demodualize, qui peuvent être un peu compliquées et donc potentiellement imprudentes à faire dans une exception. Vous pouvez également ajouter plus d'arguments à la signature de la méthode, si vous en avez besoin.

Remplacer la stratégie #to_s et non #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 les rails. Comme indiqué ci-dessus, il utilise le demodualize,underscore et les humanize ActiveSupportméthodes. 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 l'implémentation ci-dessus. La classe levant les exceptions est une fausse API pour Cloudinary. Il suffit de vider l'une des stratégies ci-dessus dans votre console de rails, puis 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
Tchad M
la source
6

Votre idée est juste, mais la façon dont vous l'appelez est fausse. Ça devrait être

raise MyCustomError.new(an_object, "A message")
Sawa
la source
D'accord, je pensais que le message que vous avez donné était un deuxième paramètre du raisemot - clé ou quelque chose.
MarioDS
Vous avez redéfini initializepour prendre deux arguments. newtransmet les arguments à initialize.
sawa
Ou, vous pouvez omettre les parenthèses.
sawa
Je comprends que peu, mais l'affiche du sujet que je lié à ma question , il fait comme ceci: raise(BillRowError.new(:roamingcalls, @index), "Roaming Calls field missing"). Il appelle donc raiseavec deux paramètres: un nouvel BillRowErrorobjet et son message. Je suis juste confus par la syntaxe ... Sur d'autres tutoriels, je le vois toujours comme ceci:raise Error, message
MarioDS
1
Le problème n'est pas avec combien d'arguments vous passez raise; c'est assez flexible. Le problème est que vous avez décidé initializede prendre deux arguments et n'en avez donné qu'un. Regardez dans votre exemple. BillRowError.new(:roamingcalls, @index)reçoit deux arguments.
sawa
4

Je voulais faire quelque chose de similaire. Je voulais passer un objet à #new et définir le message en fonction d'un traitement de l'objet passé. 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 :messagecela ne fonctionnera pas. Pour résoudre 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 le #message.

Huliax
la source