¿Cómo creo interfaces de error idiomáticas en Ruby?

7

Estoy desarrollando una aplicación Ruby on Rails. La aplicación contiene un servicio que envuelve una API REST externa, llamada desde un controlador, con varios estados de error posibles. La implementación actual devuelve el cuerpo de la respuesta en caso de éxito y, de lo contrario, genera una excepción general para todos los servicios.

Quiero refactorizar esto para poder distinguir entre los errores de red y otros, y los errores de autorización causados por el cliente que proporciona parámetros no válidos. ¿Cuál es la forma más idiomática de hacer esto? ¿Qué ha resultado en el código más mantenible en su experiencia?

A continuación hay algunas alternativas que he considerado.

Excepciones en todo

  

Se recomienda que una biblioteca tenga una subclase de StandardError o RuntimeError y que se hereden de ella tipos de excepción específicos. Esto permite al usuario rescatar un tipo de excepción genérico para detectar todas las excepciones que la biblioteca puede generar, incluso si las versiones futuras de la biblioteca agregan nuevas subclases de excepción.

De la documentación oficial de Ruby .

El código completo para mi servicio actualmente es de 39 líneas y no se puede considerar una biblioteca, pero esta estrategia aún podría ser aplicable. Una posible implementación se detalla a continuación.

class MyService
  def self.call(input)
    res = do_http_call input

    if res and res.code == 200
      res.body
    elsif res and res.code == 401
      fail MyServiceAuthenticationError
    else
      fail MyServiceError
    end
  end
end

class MyServiceError < StandardError
end

class MyServiceAuthenticationError < MyServiceError
end

Desde otros idiomas, este enfoque no parece correcto. A menudo escuché el mantra "reservar excepciones para casos excepcionales", por ejemplo, en Código Completo (Steve McConnell, 2ª edición, pág. 199):

  

Lanzar una excepción en condiciones que son verdaderamente excepcionales

     

Las excepciones deben reservarse para condiciones que son verdaderamente   excepcional - en otras palabras, para condiciones que no pueden ser abordadas por   Otras prácticas de codificación. Las excepciones se utilizan en circunstancias similares.   a las afirmaciones - para eventos que no son solo infrecuentes sino para eventos   eso debería nunca ocurrir.

¿Son las excepciones realmente errores excepcionales? es una discusión de este tema. Las respuestas proporcionan consejos variados, y la la respuesta de S.Lott declara explícitamente "No utilice excepciones para validar las opiniones de los usuarios", que Creo que es más o menos lo que equivale a la estrategia descrita anteriormente.

Símbolos para errores "no excepcionales"

Mi primera intuición es usar excepciones para los errores. Quiero aumentar la pila y los símbolos para los resultados que la persona que llama puede esperar y quiere manejar.

class MyService
  def self.call(input)
    res = do_http_call input

    if res.code == 200
      res.body
    elsif res.code == 401
      :invalid_authentication
    else
      fail MyServiceError
    end
  end
end

class MyServiceError < StandardError
end

Al igual que las excepciones, es fácil de extender con errores adicionales.

Sin embargo, podría conducir a problemas de mantenimiento. Si se agrega un nuevo valor de retorno de símbolo y la persona que llama no se modifica, el símbolo de error podría interpretarse silenciosamente como retorno exitoso ya que el valor de retorno exitoso es una cadena. Aunque no sé cuán realista es esto en la práctica.

Además, este enfoque puede considerarse más fuerte junto con su interlocutor. Si un error debe aumentar la pila de llamadas o ser manejado por la persona que llama de inmediato, podría decirse que no es algo que la persona a la que se debe llamar.

Falso en error

Un ejemplo de este enfoque es ActiveRecord::Base#save .

  • Si la operación es exitosa, devuelve el resultado, o verdadero en el caso de #save .
  • Si las validaciones fallan, devuelve falso.
  • Si se produce algún tipo de error inesperado, como la codificación de campos con UTF-8 en #save , se lanza una excepción.
class MyService
  def self.call(input)
    res = do_http_call input

    if res.code == 200
      res.body
    elsif res.code == 401
      false
    else
      fail MyServiceError
    end
  end
end

class MyServiceError < StandardError
end

En general, no me gusta esta estrategia ya que false no tiene ningún significado semántico y es imposible distinguir entre errores.

De otra manera

¿Hay otra forma superior?

    
pregunta jacwah 08.01.2017 - 00:09

2 respuestas

1

Tal vez estés buscando algo como un objeto de estado.

Por ejemplo:

class MyService
  def self.call(input)
    Client.call(input) do |status|
      status.on_success do |response|
        response.body
      end

      status.on_not_authorized do
        # do something when not authorized
      end

      status.on_error do
        # do something then there's an error
      end
    end
  end
end

class Client
  def self.call(input)
    res = do_http_call(input)

    if res.code == 200
      yield RequestStatus.success(res)
    elsif res.code == 401
      yield RequestStatus.not_authorized
    else
      yield RequestStatus.error
    end
  end
end

class RequestStatus
  def self.success(response)
    new(:success, response)
  end

  def self.not_authorized
    new(:not_authorized)
  end

  def self.error
    new(:error)
  end

  def initialize(status, response = nil)
    @status = status
    @response = response
  end

  def on_success
    yield(@response) if @status == :success
  end

  def on_not_authorized
    yield if @status == :not_authorized
  end

  def on_error
    yield if @status == :error
  end
end

No sé si este patrón es mejor por sí mismo, pero si quiere evitar las excepciones o los símbolos, esta podría ser una buena alternativa.     

respondido por el Rorshark 08.01.2017 - 02:24
1

Una buena manera que he visto para organizar excepciones en una biblioteca de Ruby es en la biblioteca de twitter de sferik.

enlace

Usando el mecanismo de creación dinámica de clases de Ruby con Class.new(ParentClass) Es muy fácil razonar acerca de la jerarquía de clases.

Las excepciones relacionadas con el cliente se heredan de ClientError. Las excepciones relacionadas con el servidor se heredan de ServerError. ClientError y ServerError heredan de Twitter :: Error

Los códigos de respuesta HTTP se asignan a clases de error y se generan al recibir la respuesta HTTP:

@parser = Http::Parser.new(http_response)
error = Twitter::Error::ERRORS[@parser.status_code]
raise error if error

He abreviado el código para mostrar solo las partes importantes:

module Twitter
  class Error < StandardError
    ...    
    ClientError = Class.new(self)
    BadRequest = Class.new(ClientError)
    Unauthorized = Class.new(ClientError)
    RequestEntityTooLarge = Class.new(ClientError)
    NotFound = Class.new(ClientError)
    NotAcceptable = Class.new(ClientError)
    UnprocessableEntity = Class.new(ClientError)
    TooManyRequests = Class.new(ClientError)

    Forbidden = Class.new(ClientError)
    AlreadyFavorited = Class.new(Forbidden)
    AlreadyRetweeted = Class.new(Forbidden)
    DuplicateStatus = Class.new(Forbidden)


    ServerError = Class.new(self)
    InternalServerError = Class.new(ServerError)
    BadGateway = Class.new(ServerError)
    ServiceUnavailable = Class.new(ServerError)
    GatewayTimeout = Class.new(ServerError)

    ERRORS = {
      400 => Twitter::Error::BadRequest,
      401 => Twitter::Error::Unauthorized,
      403 => Twitter::Error::Forbidden,
      404 => Twitter::Error::NotFound,
      406 => Twitter::Error::NotAcceptable,
      413 => Twitter::Error::RequestEntityTooLarge,
      422 => Twitter::Error::UnprocessableEntity,
      429 => Twitter::Error::TooManyRequests,
      500 => Twitter::Error::InternalServerError,
      502 => Twitter::Error::BadGateway,
      503 => Twitter::Error::ServiceUnavailable,
      504 => Twitter::Error::GatewayTimeout,
    }.freeze
    ...
  end
end
    
respondido por el Shiyason 19.02.2017 - 06:49

Lea otras preguntas en las etiquetas