web-dev-qa-db-ja.com

Rubyで慣用的なエラーインターフェイスを作成するにはどうすればよいですか?

私はRuby on Rails app。を開発しています。このアプリには、外部REST APIから呼び出されるサービスが含まれています現在の実装では、成功時に応答本文を返し、それ以外の場合はサービス固有のキャッチオール例外を発生させます。

これをリファクタリングして、ネットワークエラーなどと、クライアントが無効なパラメータを指定したことが原因で発生する認証エラーとを区別できるようにしたいと思います。これを行う最も慣用的な方法は何ですか?あなたの経験の中で最も保守しやすいコードをもたらしたものは何ですか?

以下は私が検討したいくつかの代替案です。

全体の例外

ライブラリには、StandardErrorまたはRuntimeErrorの1つのサブクラスを持たせ、特定の例外タイプを継承させることをお勧めします。これにより、ライブラリの将来のバージョンで新しい例外サブクラスが追加された場合でも、ユーザーがジェネリック例外タイプをレスキューして、ライブラリで発生する可能性があるすべての例外をキャッチできます。

official Ruby documentation から。

私のサービスの完全なコードは現在39行であり、ライブラリーと見なすことはできませんが、この戦略はまだ適用できる可能性があります。可能な実装を以下に示します。

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

他の言語から来るこのアプローチは正しくありません。私は、たとえばコードコンプリート(Steve McConnell、第2版、p。199)で、「例外的なケースのために例外を予約する」というマントラをよく耳にします。

本当に例外的な条件で例外をスローします

例外は、本当に例外的な条件、つまり、他のコーディング方法では対処できない条件のために予約する必要があります。例外は、アサーションと同様の状況で使用されます。まれなイベントだけでなく、決して発生しないイベントのイベントです。

例外は本当に例外的なエラーですか? はこのトピックの説明です。回答はさまざまなアドバイスを提供し、 S.Lottの回答 は、「例外を使用してユーザー入力を検証しないでください」と明示的に述べています。これは、上で概説した戦略とほぼ同じです。

「例外的でない」エラーの記号

私の最初の直感は、スタックをバブルアップしたいエラーに例外を使用し、呼び出し元が予期し、処理したい結果のシンボルを使用することです。

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

全体の例外と同様に、これは追加のエラーで簡単に拡張できます。

ただし、保守性の問題が発生する可能性があります。新しいシンボルの戻り値が追加され、呼び出し元が変更されていない場合、成功の戻り値は文字列であるため、エラーシンボルは成功した戻りとして静かに解釈される可能性があります。これが実際にどれほど現実的であるかはわかりません。

さらに、このアプローチは、その呼び出し元と結びついてより強力であると考えることができます。エラーが呼び出しスタックを泡立たせるか、直接の呼び出し元で処理するかは、呼び出し先が自分自身に関係するべき問題ではないことは間違いありません。

エラーの場合はfalse

このアプローチの例はActiveRecord::Base#saveです。

  • 操作が成功すると結果が返され、#saveの場合はtrueが返されます。
  • 検証が失敗した場合はfalseを返します。
  • #saveでフィールドをUTF-8でエンコードするなど、何らかの予期しないエラーが発生した場合、例外がスローされます。
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

falseには意味的な意味がなく、エラーを区別することができないため、私は一般的にこの戦略を嫌います。

別の方法

別の優れた方法はありますか?

6
jacwah

Rubyライブラリで例外を整理するために私が見た本当に良い方法は、sferikのTwitterライブラリにあります。

https://github.com/sferik/Twitter/blob/master/lib/Twitter/error.rb

Rubyの動的クラス作成メカニズムとClass.new(ParentClass)を併用することで、クラス階層について非常に簡単に推論できます。

クライアント関連の例外はClientErrorから継承します。サーバー関連の例外はServerErrorを継承します。 ClientErrorとServerErrorはどちらもTwitter :: Errorを継承しています

HTTP応答コードはエラークラスにマップされ、HTTP応答の受信時に発生します。

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

重要な部分のみを表示するようにコードを簡略化しました。

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
1
Shiyason

多分あなたはステータスオブジェクトのようなものを探しています。

例えば:

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

このパターンがbetter自体であるかどうかはわかりませんが、例外の発生を避けたり、シンボルを渡したりしたくない場合は、これが良い代替手段になる可能性があります。

1
Rorshark