私は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
全体の例外と同様に、これは追加のエラーで簡単に拡張できます。
ただし、保守性の問題が発生する可能性があります。新しいシンボルの戻り値が追加され、呼び出し元が変更されていない場合、成功の戻り値は文字列であるため、エラーシンボルは成功した戻りとして静かに解釈される可能性があります。これが実際にどれほど現実的であるかはわかりません。
さらに、このアプローチは、その呼び出し元と結びついてより強力であると考えることができます。エラーが呼び出しスタックを泡立たせるか、直接の呼び出し元で処理するかは、呼び出し先が自分自身に関係するべき問題ではないことは間違いありません。
このアプローチの例はActiveRecord::Base#save
です。
#save
の場合はtrueが返されます。#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
には意味的な意味がなく、エラーを区別することができないため、私は一般的にこの戦略を嫌います。
別の優れた方法はありますか?
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
多分あなたはステータスオブジェクトのようなものを探しています。
例えば:
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自体であるかどうかはわかりませんが、例外の発生を避けたり、シンボルを渡したりしたくない場合は、これが良い代替手段になる可能性があります。