😎
Ruby コードの可読性を上げるために(インターフェース編)
可読性とは
プログラムのソースコードを人間が読んだときの、その目的や処理の流れの理解しやすさ
当たり前にやるべきこと(この記事では書かないこと)
Formatter/Linter を使う
- Ruby なら RuboCop
- JavaScript/TypeScript なら ESLint & Prettier
などが定番の Formatter/Linter です。
基本的に使わない理由はないので使いましょう。
他に考えてほしいこと
インターフェースを明確にする
例として、以下の「WebAPI から認証トークンを取得する」クラスについて考えます。
class AuthTokenRepository
def create(params)
Faraday.new.post('/api/v1/auth_token', params.to_json)
end
end
引数の定義は詳細に
「params に指定する内容はこのURLの API Reference を見てください」とでもコメントを書いてくれていればまだマシですが、
上記のメソッド定義からは「どんな引数を渡せばいいのか」が全くわかりません。
コメントが有ったとしても、API 定義を確認しに行くための時間が必要になります。
「クラス/メソッド定義さえ読めば使える」が理想の状態だと思います。
冗長であっても、引数は詳細に定義しましょう。
class AuthTokenRepository
def create(user_id:, password:)
Faraday.new.post('/api/v1/auth_token', { user_id: user_id, password: password }.to_json)
end
end
params が ネストされてたらどうするの
Faraday.new.post('/api/v1/auth_token', { user: { id: user_id, password: password } }.to_json)
→ 引数として表現されている必要はなく、コードから必要な要素が読み取れる形で表現されていればよいです。
- フラットに書く
class AuthTokenRepository def create(user_id:, user_password:) Faraday.new.post('/api/v1/auth_token', { user: { id: user_id, password: user_password } }.to_json) end end
- バリデーションで表現する
class AuthTokenRepository class ValidationError < ::StandardError; end def create(params) raise ValidationError unless valid_params_for_create?(params) Faraday.new.post('/api/v1/auth_token', params.to_json) end private def valid_params_for_create?(params) !(params.dig(:user, :id).nil? || params.dig(:user, :password).nil?) end end
メモ
Ruby 3.0 以降であれば、.rbs
ファイルを作って型を定義するのも良いと思います。
私が仕事で参画している PJ ではまだ少し先になりそうですが。。
正常系の戻り値をカプセル化しよう
まだ問題はあります。
- 戻り値が
Faraday::Response
のインスタンスになっている- Faraday gem の API を知っていないと使えない
- API レスポンスの構造を知っていないと目的のデータが取り出せない
あなたが個人で開発しているサービスであれば好きにすればよいですが、
チームで開発しているサービスの場合、これは新規参画したメンバーがコードを理解するための障壁になります。
カプセル化を行うことで、上記の知識を AuthTokenRepository
class 内に隠蔽しましょう。
class AuthToken
def initialize(token:, expired_at:)
...
end
end
class AuthTokenRepository
def create(user_id:, password:)
response = Faraday.new.post('/api/v1/auth_token', { user_id: user_id, password: password }.to_json)
AuthToken.new(token: response.body['token'], expired_at: response.body['expired_at'])
end
end
準正常系/異常系のエラーをカプセル化しよう
忘れていました。
- raise される可能性があるエラーが
Faraday::Error
のサブクラスである
こちらについてもカプセル化を行いましょう。
class AuthTokenRepository
class Error < ::StandardError; end
class FailedToCreateError < Error; end
class UnexpectedError < Error; end
def create(user_id:, password:)
response = Faraday.new.post('/api/v1/auth_token', { user_id: user_id, password: password }.to_json)
AuthToken.new(response.body)
rescue Faraday::ClientError => e
raise FailedToCreateError, e.message
rescue => e
raise UnexpectedError, e.message
end
end
おまけ:カプセル化の嬉しい副作用
カプセル化することにより、可読性が上がるだけでなく、
- WebAPI のインターフェースが変わった
- 外部サービスではなく、DB からユーザー情報を取得して認証を行うことになった
などの変更に対し「改修の影響範囲を AuthTokenRepository
に閉じ込めることができる」、
つまり変更への耐性が高くなります。
# WebAPI のレスポンスフォーマットが変わった
class AuthTokenRepository
def create(user_id:, password:)
response = Faraday.new.post('/api/v1/auth_token', { user_id: user_id, password: password }.to_json)
AuthToken.new(
token: response.body['authToken']['token'],
expired_at: response.body['authToken']['expired_at']
)
rescue => e
# 省略
end
end
# DB を使いたい
class AuthTokenRepository
def create(user_id:, password:)
hashed_password = hash(password)
user = User.find_by!(user_id: user_id, password: hashed_password)
token, expired_at = create_token_for_user(user)
AuthToken.new(token: token, expired_at: expired_at)
rescue ActiveRecode::RecordNotFound => e
raise FailedToCreateError, e.message
rescue => e
raise UnexpectedError, e.message
end
end
Discussion