Ruby コードの可読性を上げるために(インターフェース編)

4 min read読了の目安(約4100字

可読性とは

https://ja.wikipedia.org/wiki/可読性

プログラムのソースコードを人間が読んだときの、その目的や処理の流れの理解しやすさ

当たり前にやるべきこと(この記事では書かないこと)

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)

→ 引数として表現されている必要はなく、コードから必要な要素が読み取れる形で表現されていればよいです。

  1. フラットに書く
    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
    
  2. バリデーションで表現する
    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 内に隠蔽しましょう。

https://ja.wikipedia.org/wiki/カプセル化
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