コードレビューと3R — チームで品質を上げる
コードレビューの目的を再確認する
「ユイさん、今日から僕のPRレビューに入ってもらえますか?」
後輩のケンタから声がかかった。入社半年のケンタは意欲的だが、コードレビューを受けるのがまだ少し怖そうだった。
ユイはPRを開いて、しばらく沈黙した。コードは動きそうだが、何かが引っかかる。でも「何かが引っかかる」だけではレビューコメントにならない。どうすれば良いレビューができるのか?
田中さんに聞くと、「コードレビューの目的は何だと思う?」と聞き返された。
- バグを見つけること?
- コーディング規約の確認?
- 知識の共有?
「全部正しい。でも根本は——このコードが将来変更できるかどうかを確認することだ。動くコードと、変更できるコードは別物。3Rのレンズでレビューするんだ」
INFO
コードレビューはコードへの批評であって、人への批評ではありません。「あなたのコード」ではなく「このコード」と表現するだけで、受け取りやすさが大きく変わります。目標は「良いコードにすること」であり、「この人を批判すること」ではありません。
3Rレビューチェックリスト
ユイはチームのPRテンプレートに、3Rチェックリストを追加した。
# コードレビューチェックリスト(.github/PULL_REQUEST_TEMPLATE.md)
## 変更の概要
<!-- この PR は何をしているか 1〜2文で -->
## レビュワー向けチェックリスト
### R1: Readability(可読性)
- [ ] 変数名・メソッド名を読むだけで意図が伝わるか
- [ ] 1つのメソッドが1つのことだけをしているか(目安: 15行以内)
- [ ] 「なぜ」が不明な箇所にコメントがあるか
- [ ] マジックナンバーが定数化されているか
- [ ] Railsの規約に従っているか(scope, callback の使い方)
### R2: Reusability(再利用性)
- [ ] 同じロジックが複数箇所にコピーされていないか
- [ ] 共通化できる処理は既存の抽象に乗れているか
- [ ] 新しいService/Concern/Gemとして切り出す価値があるか
### R3: Refactorability(リファクタリング可能性)
- [ ] 新しいユニットテストが書かれているか
- [ ] 変更の影響範囲が限定されているか(疎結合か)
- [ ] 将来の変更を予測した過度な設計になっていないか(YAGNI)
- [ ] テストが依存する外部サービスはモックされているか
### テスト
- [ ] テストがグリーンになっているか(CIの確認)
- [ ] カバレッジが下がっていないか
- [ ] エッジケース(空の配列、nil、0など)がテストされているか良いレビューコメントの書き方
レビューコメントは「問題を指摘する」だけでなく「改善の方向を示す」ことが大切だ。
# NG例(批判だけで改善案がない)
「このメソッドは読みにくい」
# NG例(感情的)
「なぜこんなコードを書いたの?」
# NG例(漠然としている)
「もっとRailsらしく書いてほしい」
# OK例(問題と改善案を一緒に、コードで示す)
「このメソッドが20行あり、3つの処理が混在しています。
apply_discount, calculate_tax, build_receipt に分けると
各処理が独立してテストしやすくなりませんか?
例えば:
def process_payment(order)
apply_discount(order)
calculate_tax(order)
build_receipt(order)
end
」
# OK例(質問形式で相手の意図を確認してから改善提案)
「このcaseの分岐は将来また増える可能性がありますか?
もしそうなら、Strategyパターンにしておくと追加が楽になりますが
いかがでしょうか?」
# OK例(賞賛も伝える——良いコードは積極的に認める)
「このParameter Objectの導入、素晴らしいですね!
引数が5つから1つになって、呼び出し側が格段に読みやすくなりました。」
コメントの重み付け(prefix を使う)
すべてのコメントが同じ重要度ではない。prefix を使って優先度を伝える。
# 必須の変更(マージ前に直してほしい)
[MUST] 変数 u が何を指すかわかりません。order_user など意図がわかる名前にしてください
# 推奨(直した方が良いが、任意)
[SHOULD] このif文の入れ子が深いので、ガード節で整理すると読みやすくなります
# 提案(議論の余地がある)
[NIT] 個人的にはServiceクラスより Concern の方が適切かもと思いますが、どうでしょうか
# 疑問(理解できていない、情報が欲しい)
[?] このメソッドを呼ぶケースはどのくらいの頻度を想定していますか?
# 称賛(良い実装を認める)
[NICE] このパターンは再利用性が高くて良いですね!
実際のレビュー: ケンタのPRをレビューする
実際にケンタのPRをレビューしてみよう。
# ケンタのコード(PRの内容)
def process_refund(order_id, reason)
o = Order.find(order_id)
if o.status == 'completed' && o.created_at > 30.days.ago
o.status = 'refunded'
o.refund_reason = reason
o.save
r = Refund.create(order_id: o.id, amount: o.total, reason: reason)
UserMailer.refund_notification(o.user.email, o.id, r.id).deliver_now
PaymentGateway.refund(o.payment_id, o.total)
true
else
false
end
endユイはチェックリストに沿ってレビューコメントを書いた。
[MUST] R1: Readability — 変数名
o より order、r より refund という変数名の方が次の行を読む前から意図が伝わります。
Before: o = Order.find(order_id)
After: order = Order.find(order_id)
[MUST] R1: Readability — メソッドが複数の責任を持っています
このメソッドは「返金可能チェック」「ステータス更新」「返金レコード作成」「メール通知」「決済ゲートウェイへの返金」の5つをしています。Extract Methodを検討しませんか?
[MUST] R2: Reusability — ビジネスルールの定数化
30.days.ago はビジネスルール(30日間の返金可能期間)です。定数化すると意図が伝わりやすくなります。
Before: o.created_at > 30.days.ago
After: REFUNDABLE_PERIOD = 30.days → order.created_at > REFUNDABLE_PERIOD.ago
[SHOULD] R3: Refactorability — deliver_now は避けた方が良いです
deliver_now はテストでメール送信が実際に実行されます。deliver_later に変更するとJobとして非同期化でき、テストでは have_enqueued_mail で検証できます。
[MUST] R3: Refactorability — テストがありません
このメソッドのテストはありますか?特に:
- 「30日以上前の注文は返金不可」のケース
- 「返金成功時にメールが送られる」のケース
- 「決済ゲートウェイへの返金」のケース
これらをカバーするテストがないと、次回変更するときに動作確認ができません。
[NIT] 例外ではなく ServiceResult でエラーを伝えた方がコントローラが扱いやすいです
# 現状: true/false
return false
# 提案: ServiceResult でエラー理由も伝える
ServiceResult.failure('返金期限が過ぎています')レビュー後: 改善されたコード
ケンタはレビューコメントを受けて、コードを大幅に改善した。
# After: ユイのレビューを受けて改善したケンタのコード
class RefundService
REFUNDABLE_PERIOD = 30.days
def initialize(order)
@order = order
end
def call(reason:)
return ServiceResult.failure('注文が完了状態ではありません') unless @order.completed?
return ServiceResult.failure('返金期限(30日)が過ぎています') unless within_refundable_period?
ActiveRecord::Base.transaction do
create_refund_record(reason)
update_order_status(reason)
notify_customer
process_gateway_refund
end
ServiceResult.success(@refund)
rescue PaymentGateway::Error => e
ServiceResult.failure("決済ゲートウェイエラー: #{e.message}")
end
private
def within_refundable_period?
@order.created_at > REFUNDABLE_PERIOD.ago
end
def create_refund_record(reason)
@refund = Refund.create!(
order: @order,
amount: @order.total,
reason: reason
)
end
def update_order_status(reason)
@order.update!(status: :refunded, refund_reason: reason, refunded_at: Time.current)
end
def notify_customer
RefundMailer.notification(@order.user, @order, @refund).deliver_later
end
def process_gateway_refund
PaymentGateway.refund(@order.payment_id, @order.total)
end
end# テストも追加した
RSpec.describe RefundService do
let(:order) { create(:order, :completed, created_at: 10.days.ago, total: 5_000) }
subject(:service) { described_class.new(order) }
describe '#call' do
context '返金可能な注文の場合' do
it '返金を成功させる' do
result = service.call(reason: '商品の不具合')
expect(result).to be_success
end
it '注文のステータスをrefundedに更新する' do
service.call(reason: '商品の不具合')
expect(order.reload.status).to eq('refunded')
end
it 'Refundレコードを作成する' do
expect { service.call(reason: '商品の不具合') }.to change(Refund, :count).by(1)
end
it '顧客へ通知メールをキューに追加する' do
expect { service.call(reason: '商品の不具合') }.to have_enqueued_mail(RefundMailer, :notification)
end
end
context '30日を超えた注文の場合' do
let(:order) { create(:order, :completed, created_at: 31.days.ago, total: 5_000) }
it '失敗を返す' do
result = service.call(reason: '返品希望')
expect(result).not_to be_success
expect(result.error).to include('返金期限')
end
it 'Refundレコードを作成しない' do
expect { service.call(reason: '返品希望') }.not_to change(Refund, :count)
end
end
context '完了していない注文の場合' do
let(:order) { create(:order, :pending, total: 5_000) }
it '失敗を返す' do
result = service.call(reason: 'キャンセル希望')
expect(result).not_to be_success
end
end
end
end「すごく良くなりました」ケンタが言った。「レビューを受けるのが怖かったんですが、具体的な改善提案があると学びになります」
「コードレビューはチーム全体が3Rを学ぶ場所だ」田中さんが加えた。「ユイさんが示した改善のパターン——変数名、メソッド分割、テスト——これが次のPRでケンタさんの自然な書き方になる。それがチームの文化として積み重なっていく」
自動チェックで「機械的なレビュー」を排除する
人間のレビュワーは「本質的な設計の議論」に集中すべきだ。インデントや命名規則の指摘は機械に任せる。
# .github/workflows/code_quality.yml
name: Code Quality Check
on:
pull_request:
branches: [main, develop]
jobs:
rubocop:
name: RuboCop(静的解析)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Run RuboCop
run: bundle exec rubocop --format github
# PRに直接インラインコメントが付く(--format github)
test_and_coverage:
name: テストとカバレッジチェック
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Setup database
run: bundle exec rails db:setup RAILS_ENV=test
- name: Run RSpec
run: bundle exec rspec --format progress
env:
COVERAGE: 'true'
- name: カバレッジ閾値チェック
run: |
COVERAGE=$(cat coverage/.last_run.json | jq '.result.line')
echo "Current coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "❌ Coverage ${COVERAGE}% is below threshold 80%"
exit 1
fi
echo "✅ Coverage ${COVERAGE}% meets threshold"# .rubocop.yml — チームの合意事項を設定ファイルで管理
AllCops:
NewCops: enable
TargetRubyVersion: 3.2
Exclude:
- 'db/schema.rb'
- 'db/migrate/**/*'
- 'bin/**/*'
- 'vendor/**/*'
# === Readability 関連 ===
Metrics/MethodLength:
Max: 15
Description: 'R1: メソッドは15行以内(単一責任の原則)'
Metrics/CyclomaticComplexity:
Max: 8
Description: 'R3: 条件分岐は8以下(テストケース数の目安)'
Metrics/AbcSize:
Max: 20
# 命名規則
Naming/VariableNumber:
EnforcedStyle: snake_case
# === Reusability 関連 ===
Rails/HasAndBelongsToMany:
Enabled: true
Description: 'has_many :through の使用を推奨'
Rails/InverseOf:
Enabled: true
# === 安全性 ===
Rails/DynamicFindBy:
Enabled: true
Security/YAMLLoad:
Enabled: true
Rails/SkipsModelValidations:
Enabled: true
Description: 'update_column等はバリデーションをスキップするため要注意'Danger: PRレビューの自動化をさらに進める
# Dangerfile — PRルールの自動チェック
# https://danger.systems/ruby/
# PRサイズチェック(大きすぎるPRを警告)
if git.lines_of_code > 500
warn("このPRは#{git.lines_of_code}行の変更があります。分割を検討してください")
end
# テストファイルなしの変更を警告
has_app_changes = !git.modified_files.grep(%r{app/}).empty?
has_test_changes = !git.modified_files.grep(%r{spec/}).empty?
if has_app_changes && !has_test_changes
warn("app/以下の変更がありますが、spec/に対応するテストがありません")
end
# PRテンプレートの3Rチェックリストが記入されているかチェック
if github.pr_body.length < 100
warn("PRの説明が短すぎます。何を変更したか、なぜ変更したかを記載してください")
end
# CHANGELOGの更新チェック(外部公開Gemの場合)
if git.modified_files.include?('lib/discount_engine/version.rb')
warn("version.rbが更新されましたが、CHANGELOG.mdが更新されていません") unless git.modified_files.include?('CHANGELOG.md')
endチームへの3R文化の広め方
「3Rはひとりで実践しても限界がある」田中さんが言った。「チームの文化にする必要がある」
ユイはチームの3R普及のために、以下の活動を始めた。
段階的な普及ステップ:
Phase 1(1ヶ月目): 個人実践
- 自分のPRで3Rを実践する
- レビューで3Rの観点からコメントする
Phase 2(2ヶ月目): チームへの共有
- 週次の技術共有会で1つのパターンを紹介する
- 「今週の3R改善」を10分で話す
Phase 3(3ヶ月目): チームの標準化
- PRテンプレートに3Rチェックリストを追加
- .rubocop.ymlでルールを自動化
- レビューガイドラインをドキュメント化
Phase 4(4ヶ月目以降): 文化として定着
- 新入社員のオンボーディングに3Rを組み込む
- 四半期ごとに品質指標(カバレッジ、RuboCop違反数)を計測・共有
半年後、新入社員のヒロキが初めてのPRを出したとき、レビューコメントは自然と3Rの観点で書かれていた。「このメソッド名だと意図が伝わりにくいです(R1)」「この処理は既にDiscountServiceにあります(R2)」——チームの共通言語が生まれていた。
「最後に、インフラレベルの3Rを見ていこう」田中さんが言った。