mybook

エピローグ — 3Rが身につくとき

3ヶ月後

「これ、見てください」

ユイが田中さんのデスクに近づき、ターミナルを見せた。

# 3ヶ月前: 変更頻度が高いファイルTOP5
git log --format=format: --name-only | sort | uniq -c | sort -rg | head -5
# 89 app/models/order.rb             ← Fat Model(1247行)
# 67 app/services/discount_service.rb ← 重複コードの源
# 45 app/controllers/orders_controller.rb ← ビジネスロジックが漏れ出ている
# 38 app/models/user.rb              ← 別のFat Model
# 31 app/helpers/application_helper.rb ← 雑多なメソッド置き場
 
# 3ヶ月後(同じコマンド)
git log --format=format: --name-only | sort | uniq -c | sort -rg | head -5
# 28 spec/services/order_creation_service_spec.rb ← テストが増えた!
# 23 app/services/order_creation_service.rb       ← 分割・整理済み
# 18 app/models/concerns/user/loyalty_program.rb  ← Concern
# 15 spec/models/concerns/user/loyalty_program_spec.rb
# 12 app/services/discount_service.rb

「変更頻度が最も高いのはテストファイルになりましたね」ユイが言った。「コードを変えるたびにテストも書くようになったから。そして変更ファイルが分散した——1000行のFat Modelを1箇所触る代わりに、複数の小さなファイルを適切な場所で触るようになった」

田中さんは微笑んだ。「数字を見てみよう」

計測: 3Rの改善を数値で確認する

3Rは「感覚の改善」ではなく「計測できる改善」だ。

# テストカバレッジの計測(SimpleCov)
COVERAGE=true bundle exec rspec
 
# Coverage report:
# +----------+--------+----------+--------+
# | Group    | Lines  | Relevant | %      |
# +----------+--------+----------+--------+
# | Services | 1,234  | 1,156    | 87.2%  |
# | Models   | 892    | 845      | 91.1%  |
# | Concerns | 456    | 432      | 94.7%  |
# +----------+--------+----------+--------+
# | Total    | 3,847  | 3,201    | 83.2%  |
# +----------+--------+----------+--------+
# RuboCopでコード品質を定量化
bundle exec rubocop --format json | jq '
  {
    total_offenses: .summary.offense_count,
    files_inspected: .summary.inspected_file_count,
    avg_per_file: (.summary.offense_count / .summary.inspected_file_count | floor)
  }
'
 
# 3ヶ月前:
# { "total_offenses": 847, "files_inspected": 124, "avg_per_file": 6 }
 
# 3ヶ月後:
# { "total_offenses": 89, "files_inspected": 156, "avg_per_file": 0 }
指標3ヶ月前3ヶ月後改善率
テストカバレッジ34%83%+49pt
RuboCop違反数847件89件-89%
最大メソッド長87行18行-79%
Fat Model行数(order.rb)1,247行94行-92%
Fat Model行数(user.rb)1,089行50行-95%
バグ修正の平均時間4.2時間1.1時間-74%
新機能の平均実装時間3.8日2.1日-45%

「バグ修正の時間が4分の1になった」田中さんが言った。「可読性が上がると、問題の特定が速くなる。再利用性が上がると、変更箇所が1箇所になる。リファクタリング可能性が上がると、テストが自動で検証してくれる。3つのRが揃うと、相乗効果が出る」

3Rが習慣になるとき

「どうすれば3Rが自然にできるようになりますか?」ユイが聞いた。

田中さんは3つのことを答えた。

習慣1: コードを書く前に「読む人」を想像する

「書く前の5秒間を使う習慣だ」

# コードを書く前に問いかける質問
 
# 変数名を付けるとき:
# 「3ヶ月後の自分がこの変数を見たとき、何のための変数かわかるか?」
 
# メソッドを作るとき:
# 「このメソッドは1つのことだけしているか?名前がその1つを表しているか?」
 
# クラスを作るとき:
# 「このクラスが変更される理由は何か?複数あるなら分割すべきか?」
 
# コードを書いた後に確認する:
# 「初めてこのコードを見る人が5分で理解できるか?」

習慣2: PRを出す前にセルフレビューする

「レビュワーに見せる前に、自分でチェックする」

# PRセルフレビューチェックリスト
 
## このPRは何をしているか?
- [ ] 1文で説明できるか(できなければ分割を検討)
- [ ] タイトルが変更内容を正確に表しているか
 
## R1チェック(Readability)
- [ ] 変数名・メソッド名を読むだけで意図が伝わるか
- [ ] マジックナンバーが定数化されているか
- [ ] メソッドが15行以内か
- [ ] コメントが「なぜ」だけを説明しているか(「何をする」ではない)
 
## R2チェック(Reusability)
- [ ] 重複コードを書いていないか(既存のService/Concernを使えているか)
- [ ] 新しいビジネスロジックが適切な場所にあるか(コントローラに漏れていないか)
 
## R3チェック(Refactorability)
- [ ] 新しいテストを書いたか
- [ ] テストがグリーンになっているか(CIの確認)
- [ ] 変更の影響範囲が限定されているか(変えた場所だけが変わっているか)
- [ ] 外部サービスへの依存が注入可能になっているか
 
## 将来の変更者へ
- [ ] なぜこの実装にしたかがコードまたはコメントから読み取れるか
- [ ] 将来変わりそうな数値・文字列が定数になっているか

習慣3: 継続的なリファクタリングを当たり前にする

ボーイスカウトの法則——来たときよりキャンプ場をきれいにして帰る」

# 毎回のPRで「少し良くする」を積み重ねる
 
# 新機能を追加するついでに:
# - 通り道のメソッド名を改善する(変数 x → order)
# - 見かけたWETなコードをDRYにする(割引ロジックの重複をServiceに抽出)
# - カバレッジが0のメソッドにテストを追加する
 
# 大規模なリファクタリングスプリントより
# 継続的な小さな改善の方が持続可能
 
# なぜなら:
# 大規模リファクタリング → 長期間コードが不安定 → チームが嫌がる → 実行されない
# 継続的な小改善 → 常に安定 → チームに受け入れられる → 継続できる

INFO

「あとでリファクタリングする」は99%実現しません。今日のPRで少しだけ良くすることを積み重ねる方が、技術的負債を確実に返済できます。「あとで」という言葉は、エンジニアリングの世界では「永遠に」を意味することが多い。

継続的改善のサイクル: 計測→改善→計測

3Rを個人の感覚に頼るのではなく、数値で追跡する仕組みを作る。

# app/services/quality_report.rb
class QualityReport
  def self.generate_and_notify
    report = new.generate
    Slack::Notifier.new(ENV['QUALITY_SLACK_WEBHOOK']).ping(
      attachments: [report.to_slack_attachment]
    )
  end
 
  def generate
    QualityMetrics.new(
      coverage: current_test_coverage,
      rubocop_offenses: rubocop_offense_count,
      fat_models: fat_model_count,
      average_method_length: average_method_length,
      measured_at: Time.current
    )
  end
 
  private
 
  def current_test_coverage
    json = JSON.parse(File.read('coverage/.last_run.json'))
    json.dig('result', 'line').round(1)
  rescue StandardError
    nil
  end
 
  def rubocop_offense_count
    output = `bundle exec rubocop --format json 2>/dev/null`
    JSON.parse(output).dig('summary', 'offense_count')
  rescue StandardError
    nil
  end
 
  def fat_model_count
    Dir['app/models/**/*.rb'].count do |f|
      File.read(f).lines.count > 200
    end
  end
 
  def average_method_length
    total_methods = 0
    total_length = 0
 
    Dir['app/**/*.rb'].each do |file|
      content = File.read(file)
      methods = content.scan(/^\s+def .+?(?=\s+(?:def |end\s*$))/m)
      methods.each do |method|
        total_methods += 1
        total_length += method.lines.count
      end
    end
 
    return 0 if total_methods.zero?
    (total_length.to_f / total_methods).round(1)
  end
end
 
QualityMetrics = Data.define(
  :coverage,
  :rubocop_offenses,
  :fat_models,
  :average_method_length,
  :measured_at
) do
  def to_slack_attachment
    {
      title: "週次コード品質レポート(#{measured_at.strftime('%Y/%m/%d')})",
      color: overall_health_color,
      fields: [
        { title: "テストカバレッジ", value: "#{coverage}%", short: true },
        { title: "RuboCop違反", value: "#{rubocop_offenses}件", short: true },
        { title: "Fat Model数", value: "#{fat_models}個", short: true },
        { title: "平均メソッド長", value: "#{average_method_length}行", short: true }
      ]
    }
  end
 
  private
 
  def overall_health_color
    return 'good' if coverage >= 80 && rubocop_offenses <= 50 && fat_models == 0
    return 'warning' if coverage >= 60
    'danger'
  end
end
# config/schedule.rb(whenever gemでcron設定)
# 毎週月曜9時にレポートをSlackに送信
every :week, at: '9:00am', roles: [:app] do
  runner "QualityReport.generate_and_notify"
end

チームへの広め方: 共通言語を作る

Loading diagram...

ユイはコードレビューで3Rを伝え、月次の技術共有会で「今月のBefore/After」を発表し、チームのPRテンプレートに3Rチェックリストを追加した。

6ヶ月後には、ミーティングで自然と「これってR1的に大丈夫?」「R2でサービス化した方が良くない?」という言葉が飛び交うようになった。3Rがチームの共通言語になった。

新入社員のヒロキが最初のPRを出したとき、コードを見た瞬間に「あ、これは変数名が意図を伝えていないな」と気づいてコメントできた。3Rがチームの文化として根付いていた。

ユイからの手紙: 新入社員研修での登壇

9ヶ月後、新入社員向けの技術研修でユイは登壇した。

「3Rは魔法ではありません。最初は時間がかかります。

良い変数名を考えるのに5分かかることがある。 メソッドを分割するのに30分追加でかかることがある。 テストを書くのに実装の2倍の時間がかかることがある。

でも3ヶ月後、バグ修正の時間が4分の1になりました。 新機能の実装が半分の時間でできるようになりました。 土日にアラートで起こされる回数が減りました。 コードを変えることへの恐怖がなくなりました。

3Rは投資です。今日の30分が、来月の3時間を節約します。」

「最初にどこから始めれば良いですか?」新入社員のひとりが聞いた。

「今日の作業で書いた変数が1つでもある。その変数の名前を1つ改善することから始めてください。xorder に。ddiscount_amount に。たった1行の変更から、3Rの旅は始まります」

3Rの本質: コードは人のために書く

# プロローグで見たコード(3Rの旅の出発点)
def p(o, u, c)
  x = Order.find(o)
  if x.st == 1
    if u.pr == true
      x.tt = x.tt * 0.8
    end
    # ...
  end
end
 
# 9ヶ月後(3Rの旅の到達点)
class OrderCompletionService
  PREMIUM_DISCOUNT_RATE = 0.20
 
  def initialize(order, user, coupon_code: nil)
    @order = order
    @user = user
    @coupon_code = coupon_code
  end
 
  def call
    return ServiceResult.failure('注文が保留中ではありません') unless @order.pending?
 
    ActiveRecord::Base.transaction do
      apply_premium_discount
      apply_coupon_if_present
      complete_order
    end
 
    ServiceResult.success(@order)
  end
 
  private
 
  def apply_premium_discount
    return unless @user.premium?
 
    discount = (@order.total * PREMIUM_DISCOUNT_RATE).round(2)
    @order.discount_amount = discount
    @order.total -= discount
  end
 
  def apply_coupon_if_present
    return unless @coupon_code.present?
 
    coupon = Coupon.find_by(code: @coupon_code)
    return unless coupon&.redeemable?
 
    coupon.apply_to(@order)
  end
 
  def complete_order
    @order.update!(status: :completed, completed_at: Time.current)
    OrderMailer.confirmation(@order.user, @order).deliver_later
  end
end

同じ機能を実現するコードだ。でも後者を読んだ人は、1分で「何をするコードか」がわかる。変更したい場所がわかる。変更しても何が壊れるかわかる。そして変更しても壊れていないことを自動で確認できる。

3Rの本質:

Readability    = 読む人へのリスペクト
Reusability    = 解いた問題を知識にして次の人に渡す
Refactorability = 未来の変更者(しばしば自分自身)への配慮

どれも「コードは人のために書く」という一点に集約される。

コンピュータはどんな書き方でも動く。
でも人間は、読めなければ理解できない。
変えられなければ前進できない。
同じ問題を何度も解くしかない。

3Rは技術的なプラクティスだが、その根本は「思いやり」だ。
チームの誰かが——それは未来の自分かもしれない——
そのコードを読んで、変えて、拡張する。
その人のために書く。
それが3Rだ。

あなたの3R旅

この本を読み終えた今、あなたの旅が始まる。

# 今日からできる3アクション
 
# アクション1: 一番読みにくいメソッドを1つ選んで名前を改善する
# Before: def p(o, u)
# After:  def process_order(order, user)
 
# アクション2: 重複している処理を1つ見つけてDRYにする
# 3箇所に同じ割引ロジック → DiscountService に集約
 
# アクション3: テストがないメソッドを1つ選んでテストを書く
# describe OrderCompletionService do
#   it '注文を完了させる' do
#     ...
#   end
# end

コードは書かれた瞬間から「過去のもの」になる。未来の誰かが——それはあなた自身かもしれない——そのコードを読んで、変えて、拡張していく。

3Rは、未来の誰かへの贈り物だ。


INFO

3Rは完璧を目指す原則ではありません。「今日少しだけ良くする」を積み重ねることが本質です。完璧なコードより、昨日より良いコードを。焦らず、着実に。旅を楽しんでください。


「書いたコードを誇りに思える日が来る。3Rはその道標だ」— 田中さん


付録: 3R クイックリファレンス

## R1: Readability チェックリスト
- [ ] 変数名が意図を表している(x → order, d → discount_amount)
- [ ] メソッド名が1つの動作を表している(process → process_order)
- [ ] ブール値メソッドが ? で終わる(premium?, active?, expired?)
- [ ] コレクションが複数形(item → items, order → orders)
- [ ] メソッドが15行以内(例外的に長い場合は分割を検討)
- [ ] ガード節で入れ子を減らしている
- [ ] コメントが「なぜ」だけを説明している(「何をする」はコードが説明する)
 
## R2: Reusability チェックリスト
- [ ] 同じロジックが3箇所以上あれば抽出(3回ルール)
- [ ] ビジネスロジックがコントローラに漏れていない
- [ ] 複数モデルに共通するロジックはConcernに切り出している
- [ ] チーム内/複数プロジェクトで共通のコードはGem化を検討
- [ ] マジックナンバーが定数になっている
 
## R3: Refactorability チェックリスト
- [ ] テストカバレッジが80%以上
- [ ] 1クラス1責任(SRP)
- [ ] 新機能追加で既存コードを変えずに済む(OCP)
- [ ] 依存性注入でテストが書ける(DIP)
- [ ] インフラはTerraform/CloudFormationで管理(IaC)
- [ ] CIでテスト・Lintが自動実行される