Rubyで読みやすく、保守しやすく、安全なコードを書くための実践的なガイドです。 コミュニティで広く使われている Ruby Style Guide と RuboCop の考え方をベースにしています。
目次
- 基本スタイル
- 命名規則
- メソッド設計
- クラスとモジュール
- コレクション操作
- 文字列
- 条件分岐と制御構文
- 例外処理
- nil の扱い
- パフォーマンス
- セキュリティ
- テスト
- 依存関係とプロジェクト管理
- ツール
- チェックリスト
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"
end2. 命名規則
| 対象 | 規則 | 例 |
|---|---|---|
| 変数・メソッド | snake_case | user_name, calculate_total |
| クラス・モジュール | CamelCase | UserAccount, PaymentService |
| 定数 | SCREAMING_SNAKE_CASE | MAX_RETRY_COUNT |
| ファイル名 | snake_case.rb | user_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
end4. クラスとモジュール
クラス内の並び順を統一する
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
endand/or/notではなく&&/||/!を使う(優先順位の罠を避ける)- 複数の値や型による分岐には
caseを使う。Ruby 3 以降はパターンマッチも有効
case response
in { status: 200, body: }
handle(body)
in { status: 404 }
not_found
in { status: 500.. }
server_error
endforループは使わず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
endrescue 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}"
end9. 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、bulletgem) - 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: 1515. チェックリスト
コードレビューやコミット前の確認用です。
-
# frozen_string_literal: trueを付けたか - 命名は意図が伝わるか(
?/!の使い方は正しいか) - メソッドは短く、一つのことだけをしているか
- ガード節で不要なネストを避けているか
- ループではなく Enumerable メソッドを使っているか
- 例外は具体的なクラスで捕捉しているか(
rescue Exceptionしていないか) - 外部入力を
eval/ シェル / SQL に直接渡していないか - 公開範囲は最小限か(不要なメソッドは
privateか) - テストを書き、すべて通っているか
- RuboCop の警告を解消したか
参考資料
- Ruby Style Guide
- Ruby 公式ドキュメント
- RuboCop
- RSpec
- 書籍『Practical Object-Oriented Design(POODR)』 Sandi Metz
- 書籍『Effective Ruby』 Peter J. Jones