Rubyで読みやすく、保守しやすく、安全なコードを書くための実践的なガイドです。 コミュニティで広く使われている Ruby Style Guide と RuboCop の考え方をベースにしています。


目次

  1. 基本スタイル
  2. 命名規則
  3. メソッド設計
  4. クラスとモジュール
  5. コレクション操作
  6. 文字列
  7. 条件分岐と制御構文
  8. 例外処理
  9. nil の扱い
  10. パフォーマンス
  11. セキュリティ
  12. テスト
  13. 依存関係とプロジェクト管理
  14. ツール
  15. チェックリスト

1. 基本スタイル

  • インデントは スペース2つ(タブは使わない)
  • 1行は 80〜120文字 以内を目安にする
  • ファイル末尾には改行を入れ、行末の空白は削除する
  • ファイル先頭にマジックコメントを書き、文字列リテラルを凍結する
# frozen_string_literal: true
  • 演算子やカンマの後、{ の前後にはスペースを入れる
# Good
sum = a + b
[1, 2, 3].each { |n| puts n }
hash = { name: "Alice", age: 30 }
 
# Bad
sum=a+b
[1,2,3].each{|n|puts n}
  • 引数がないメソッドの定義・呼び出しでは括弧を省略する
# Good
def greet
  "hello"
end
 
# Bad
def greet()
  "hello"
end

2. 命名規則

対象規則例
変数・メソッドsnake_caseuser_name, calculate_total
クラス・モジュールCamelCaseUserAccount, PaymentService
定数SCREAMING_SNAKE_CASEMAX_RETRY_COUNT
ファイル名snake_case.rbuser_account.rb
真偽値を返すメソッド末尾に ?valid?, empty?
破壊的・危険なメソッド末尾に !save!, sort!
  • 真偽値メソッドに is_ / has_ は付けない
# Good
def admin?
  role == :admin
end
 
# Bad
def is_admin?
  role == :admin
end
  • ! は「同名の安全なメソッドがあり、こちらはより危険」という場合にだけ使う
  • 省略形や1文字の変数名は避ける(短いブロック内の i, n などは許容)

3. メソッド設計

メソッドは短く、一つのことだけを行う

目安は 10行以内。長くなったら小さなメソッドに分割します。

引数はキーワード引数を活用する

引数が3つ以上、または真偽値フラグを渡す場合はキーワード引数にすると呼び出し側が読みやすくなります。

# Good
def create_user(name:, email:, admin: false)
  # ...
end
 
create_user(name: "Alice", email: "alice@example.com", admin: true)
 
# Bad: true が何を意味するか分からない
def create_user(name, email, admin = false)
end
 
create_user("Alice", "alice@example.com", true)

暗黙の戻り値を使う

最後に評価された式が戻り値になるため、末尾の return は不要です。早期リターンには return を使います。

# Good
def full_name
  "#{first_name} #{last_name}"
end
 
def discount(user)
  return 0 unless user.member?
 
  user.points * 0.01
end

ガード節でネストを減らす

# Good
def process(order)
  return if order.nil?
  return unless order.paid?
 
  ship(order)
end
 
# Bad
def process(order)
  if order
    if order.paid?
      ship(order)
    end
  end
end

4. クラスとモジュール

クラス内の並び順を統一する

class User
  # 1. extend / include / prepend
  include Comparable
 
  # 2. 定数
  ROLES = %i[admin member guest].freeze
 
  # 3. attr_* マクロ
  attr_reader :name, :email
 
  # 4. クラスメソッド
  def self.build_guest
    new(name: "Guest", email: nil)
  end
 
  # 5. initialize
  def initialize(name:, email:)
    @name = name
    @email = email
  end
 
  # 6. public インスタンスメソッド
  def <=>(other)
    name <=> other.name
  end
 
  # 7. protected / private
  private
 
  def normalize_email
    email&.downcase
  end
end

その他の指針

  • アクセサは手書きせず attr_reader / attr_writer / attr_accessor を使う
  • 外部に公開する必要のないものは private にする(attr_writer も含めて最小限の公開に)
  • 継承より コンポジション(委譲)やモジュールの mix-in を優先する
  • 状態を持たないユーティリティは module_function を使ったモジュールにする
  • 値オブジェクトには Struct や Data(Ruby 3.2+)が便利
Point = Data.define(:x, :y)   # イミュータブルな値オブジェクト
p1 = Point.new(x: 1, y: 2)
  • クラス変数 @@var は継承時に共有され予期せぬ挙動になりやすいので避け、クラスインスタンス変数を使う

5. コレクション操作

ループより Enumerable のメソッドを使う

# Good
names = users.map(&:name)
adults = users.select { |u| u.age >= 18 }
total = prices.sum
 
# Bad
names = []
users.each { |u| names << u.name }

目的に合ったメソッドを選ぶ

やりたいこと使うメソッド
変換map
絞り込みselect / filter / reject
最初の1件find / detect
変換+nil除去filter_map
集計sum / count / tally / group_by
平坦化しつつ変換flat_map
存在確認any? / all? / none? / include?
ハッシュへの変換to_h / each_with_object
# filter_map
emails = users.filter_map { |u| u.email if u.active? }
 
# tally
%w[a b a c a].tally  # => {"a"=>3, "b"=>1, "c"=>1}

リテラル

  • 文字列の配列は %w[]、シンボルの配列は %i[] を使う
  • ハッシュのキーにはシンボルを使い、新しい記法 key: value で書く
  • 存在しないキーを検知したいときは Hash#fetch を使う
config.fetch(:timeout)        # キーがなければ KeyError
config.fetch(:timeout, 30)    # デフォルト値
  • 定数として定義するコレクションは freeze する

6. 文字列

  • 連結より 式展開 を使う
# Good
"Hello, #{name}!"
 
# Bad
"Hello, " + name + "!"
  • ループ内で文字列を組み立てるときは += ではなく << や Array#join を使う(+= は毎回新しいオブジェクトを生成する)
  • 複数行の文字列にはヒアドキュメント <<~ を使う(インデントが除去される)
message = <<~TEXT
  こんにちは、#{name}さん。
  ご登録ありがとうございます。
TEXT
  • シングルクォートとダブルクォートはプロジェクト内でどちらかに統一する

7. 条件分岐と制御構文

  • 1行で済む単純な条件は 後置 if / unless を使う
send_email if user.subscribed?
  • unless に else を付けない。否定条件が複雑なら if に書き換える
# Bad
unless valid?
  error
else
  success
end
  • and / or / not ではなく && / || / ! を使う(優先順位の罠を避ける)
  • 複数の値や型による分岐には case を使う。Ruby 3 以降はパターンマッチも有効
case response
in { status: 200, body: }
  handle(body)
in { status: 404 }
  not_found
in { status: 500.. }
  server_error
end
  • for ループは使わず each を使う
  • 三項演算子は単純な場合のみ。ネストさせない

8. 例外処理

具体的な例外クラスを捕捉する

# Good
begin
  fetch_data
rescue Net::ReadTimeout, Net::OpenTimeout => e
  logger.warn("Timeout: #{e.message}")
  retry_later
end
 
# Bad: すべてを握りつぶす
begin
  fetch_data
rescue Exception
end
  • rescue Exception は Interrupt や SystemExit まで捕まえてしまうため 禁止
  • 例外を握りつぶさない。最低限ログを残すか、再送出する
  • 独自の例外は StandardError を継承した階層を作る
module MyApp
  class Error < StandardError; end
  class NotFoundError < Error; end
  class ValidationError < Error; end
end
  • 例外を制御フロー(通常の分岐)として使わない
  • リソースの解放はブロック付きメソッドや ensure で確実に行う
File.open("data.txt") do |f|
  f.each_line { |line| process(line) }
end  # ブロックを抜けると自動でクローズ
  • メソッド全体を囲む場合は begin を省略できる
def load
  JSON.parse(File.read(path))
rescue JSON::ParserError => e
  raise MyApp::ValidationError, "Invalid JSON: #{e.message}"
end

9. nil の扱い

  • ぼっち演算子 &. で nil チェックを簡潔に
user&.profile&.avatar_url

ただし &. の連鎖が長い場合は設計(デメテルの法則違反)を見直すサイン

  • デフォルト値には || / ||= を使う(ただし false も置き換わる点に注意)
@cache ||= expensive_calculation
  • ネストしたハッシュには dig を使う
params.dig(:user, :address, :city)
  • nil を返しうるメソッドは名前やドキュメントで明示し、可能なら Null Object パターンを検討する

10. パフォーマンス

  • まず 計測 してから最適化する(benchmark, benchmark-ips, stackprof, memory_profiler)
  • # frozen_string_literal: true で不要な文字列オブジェクト生成を減らす
  • 大量データや無限列には lazy を使う
(1..Float::INFINITY).lazy.map { |n| n * 2 }.select(&:even?).first(5)
  • 存在確認を何度も行う場合は Array#include?(O(n))より Set(O(1))を使う
  • 同じ計算を繰り返さないようメモ化する(||=。ただし結果が nil/false の場合は defined? を使う)
  • Rails を使う場合は N+1 クエリに注意(includes / preload、bullet gem)
  • YJIT(Ruby 3.1+ の JIT コンパイラ)の有効化を検討する: ruby --yjit

11. セキュリティ

  • eval, instance_eval(string), send にユーザー入力を渡さない
    • 動的にメソッドを呼ぶなら public_send + 許可リストを使う
  • シェル実行時は文字列ではなく 引数を配列で渡す
# Good
system("ls", "-l", user_dir)
 
# Bad: コマンドインジェクションの危険
system("ls -l #{user_dir}")
  • SQL は必ずプレースホルダを使う(文字列展開で組み立てない)
  • YAML の読み込みは YAML.safe_load を使う(YAML.load の任意オブジェクト生成に注意)
  • Marshal.load に信頼できないデータを渡さない
  • 秘密情報はコードに書かず、環境変数や認証情報ストアで管理する
  • bundler-audit や brakeman(Rails)で脆弱性を定期チェックする

12. テスト

  • テストフレームワークは RSpec または Minitest
  • 1つのテストでは1つの振る舞いを検証する
  • テストは Arrange(準備)→ Act(実行)→ Assert(検証) の構成で書く
  • テストの説明は振る舞いを表す文章にする
RSpec.describe Cart do
  describe "#total" do
    context "商品が空のとき" do
      it "0 を返す" do
        cart = Cart.new
 
        expect(cart.total).to eq(0)
      end
    end
 
    context "割引クーポンを適用したとき" do
      it "割引後の金額を返す" do
        cart = Cart.new(items: [Item.new(price: 1000)])
        cart.apply(Coupon.new(rate: 0.1))
 
        expect(cart.total).to eq(900)
      end
    end
  end
end
  • 外部APIは webmock / vcr でスタブし、テストをネットワークに依存させない
  • テストデータは factory_bot などで生成し、フィクスチャの肥大化を避ける
  • カバレッジは simplecov で計測(数値そのものより、重要なロジックが網羅されているかを重視)
  • CI で毎回テストと Lint を実行する

13. 依存関係とプロジェクト管理

  • Ruby のバージョンは .ruby-version で固定し、rbenv / asdf / mise などで管理する
  • gem は Bundler で管理し、Gemfile.lock はアプリケーションではコミットする
  • gem のバージョン指定は悲観的制約 ~> を基本にする
gem "rails", "~> 7.2"
gem "rspec", "~> 3.13", group: :test
  • 不要な gem は追加しない。追加前にメンテ状況・ダウンロード数・ライセンスを確認する
  • bundle outdated で定期的に依存を更新する
  • 標準的なディレクトリ構成に従う
my_app/
├── bin/
├── lib/
│   └── my_app/
├── spec/ (または test/)
├── Gemfile
├── Gemfile.lock
├── .rubocop.yml
└── .ruby-version

14. ツール

目的ツール
Lint / フォーマットRuboCop(rubocop -a で自動修正)、Standard
型チェックRBS + Steep、Sorbet
言語サーバRuby LSP
ドキュメントYARD
脆弱性チェックbundler-audit、Brakeman
デバッグdebug gem(binding.break)、irb
プロファイリングstackprof、memory_profiler、benchmark-ips

.rubocop.yml の最小例:

AllCops:
  NewCops: enable
  TargetRubyVersion: 3.3
  Exclude:
    - "vendor/**/*"
    - "db/schema.rb"
 
Style/Documentation:
  Enabled: false
 
Metrics/MethodLength:
  Max: 15

15. チェックリスト

コードレビューやコミット前の確認用です。

  • # frozen_string_literal: true を付けたか
  • 命名は意図が伝わるか(? / ! の使い方は正しいか)
  • メソッドは短く、一つのことだけをしているか
  • ガード節で不要なネストを避けているか
  • ループではなく Enumerable メソッドを使っているか
  • 例外は具体的なクラスで捕捉しているか(rescue Exception していないか)
  • 外部入力を eval / シェル / SQL に直接渡していないか
  • 公開範囲は最小限か(不要なメソッドは private か)
  • テストを書き、すべて通っているか
  • RuboCop の警告を解消したか

参考資料

0件のページ