mybook

プロローグ — 読めないコードとの戦い

レガシーコードに出会った日

「ユイさん、このECサイトの注文処理、ちょっと見てもらえますか?バグが出てるんですが、誰も手をつけたがらなくて」

月曜の朝9時。シニアエンジニアの田中さんが申し訳なさそうな顔でデスクに近づいてきた。入社2年目のユイにとって、「誰も手をつけたがらない」という言葉は警戒サインだった。しかし断るわけにもいかず、緊張しながらターミナルを開いた。

git checkout legacy/order-processing
git log --oneline -5
# a3f8b21 fix: quick fix for payment bug
# 7c2d104 update order stuff
# 3e9f001 changes
# 9ba2c88 fix
# 4d1f220 initial commit

コミットメッセージからすでに嫌な予感がした。「changes」「fix」——意味がない。そしてファイルを開いた瞬間、ユイは言葉を失った。

# app/models/order.rb (抜粋)
# Before: 実際にあったコード(悪い例)
def p(o, u, c)
  x = Order.find(o)
  if x.st == 1
    if u.pr == true
      x.tt = x.tt * 0.8
    end
    if c != nil
      cp = Coupon.find_by(cd: c)
      if cp && cp.ex > Time.now && cp.u == false
        x.tt = x.tt - cp.am
        cp.u = true
        cp.save
      end
    end
    x.st = 2
    x.save
    UserMailer.om(u.em, x.id).deliver_later
    return true
  else
    return false
  end
end

pousttt……。何が何を意味するのか、まったくわからない。まるで暗号文だ。

WARNING

このコードには名前を見ただけでは意味が理解できない変数が10個以上あります。バグを修正しようとすれば、まず「解読」という余分な作業が発生します。これは「技術的負債」が利子として現れた瞬間です。

「読めないコード」の本当のコスト

ユイは腰を据えてコードと格闘し始めた。変数名を1つずつメモに書き出す。まるで考古学の発掘作業のようだ。

# 2時間かけて作成した「解読表」
# o  → order_id     (Orderのid)
# u  → user         (Userオブジェクト)
# c  → coupon_code  (クーポンコード文字列)
# x  → order        (Orderオブジェクト)
# st → status       (注文ステータス 1=pending, 2=completed)
# tt → total        (合計金額)
# pr → premium?     (プレミアム会員か)
# cp → coupon       (Couponオブジェクト)
# cd → code         (クーポンコード文字列)
# ex → expired_at   (有効期限)
# am → amount       (割引額)
# u  → used?        (使用済みか)  ← uがまた別の意味で使われている!

解読を終えてやっとバグを発見した。クーポンの有効期限チェックで cp.ex > Time.now となっているが、正しくは cp.ex > Time.now ではなく cp.ex >= Time.now のはずだ(厳密には cp.ex.future? であるべき)。修正自体は1行なのに、理解するために2時間かかった。

そしてもう1つ重大な問題を発見した。u という変数がメソッドの引数では「user(ユーザーオブジェクト)」を指しているのに、クーポンの使用フラグ処理では cp.u = trueused = true)の別の意味で使われている。コードが自己矛盾を抱えている。

Loading diagram...

合計4時間15分。バグ修正そのものは5分なのに、前後の「理解」と「確認」に4時間10分費やした。これが「読めないコード」のコストだ。

作業時間
変数名の解読2時間
処理フローの把握1時間
バグ箇所の特定30分
バグの修正5分
テストと確認30分
合計4時間15分

「このコードを書いた人を恨みたくなる気持ちもわかるけど」と田中さんは言った。「でも当時は締め切りが厳しかったんだ。動くコードを書くことで精一杯で、読みやすさまで考える余裕がなかった。そして今、そのツケが回ってきている」

田中さんの言葉は正しかった。コードを書いた人は悪人ではない。状況が悪かっただけだ。でも問題はその状況が変わっても「読めないコード」は残り続け、毎回誰かが2時間の「解読作業」を強いられることだ。

レガシーコードが生まれる理由

「なぜ、こういうコードが書かれるんでしょうか?」ユイは田中さんに聞いた。

「いくつかパターンがある」田中さんがホワイトボードに書き始めた。

理由1: 時間的プレッシャー 締め切りが迫っているとき、人は「動くコード」を優先する。命名を考える5分が惜しくなる。process_order と書くより p と書く方が速い。しかしその5分のケチりが、後に何十時間もの解読作業を生む。

理由2: コンテキストの消失 コードを書いた瞬間は、x が何を指すか頭の中でわかっている。3ヶ月後、別の人(あるいは未来の自分)がそのコンテキストを持たずにコードを読む。その「コンテキストの消失」を補うのが、適切な命名だ。

理由3: 段階的な劣化 最初は良いコードだったかもしれない。でも「ちょっとした修正」が積み重なり、元の設計が崩れていく。まるできれいな部屋に「ちょっと置いておくだけ」という荷物が増えて、いつの間にか足の踏み場もなくなるように。

理由4: テストがない テストがないと、変更することへの恐怖が生まれる。「変えたら何かが壊れるかも」という恐怖が、リファクタリングを妨げる。リファクタリングできないから可読性が下がり続ける。

# 段階的な劣化の例
# バージョン1(最初はきれいだった)
def process_order(order, user)
  order.update!(status: :completed)
  OrderMailer.confirmation(user, order).deliver_later
end
 
# バージョン2(緊急修正で条件が増えた)
def process_order(order, user)
  order.update!(status: :completed)
  OrderMailer.confirmation(user, order).deliver_later unless order.test_order?
  InventoryService.decrease(order) if order.physical_items?  # 追加
end
 
# バージョン5(1年後、誰も全体を把握していない)
def process_order(order, user, skip_mail: false, force: false)
  return false if order.locked? && !force
  order.update!(status: :completed)
  unless skip_mail || order.test_order? || user.email_unsubscribed?
    OrderMailer.confirmation(user, order).deliver_later
  end
  InventoryService.decrease(order) if order.physical_items? && !order.preorder?
  CrmService.sync(user) if user.crm_enabled? && !Rails.env.test?
  Analytics.track('order.completed', order_id: order.id, user_id: user.id)
  true
end

「見てごらん」田中さんが言った。「最初は10行だったメソッドが、1年で30行に膨れ上がっている。それぞれの修正は合理的だった。でも全体として見ると、このメソッドは今何をしているのか、一度読んだだけではわからない」

INFO

コードは生き物です。時間とともに「変化圧力」を受け続け、放置すると劣化します。3Rの実践は、この劣化に抗うための継続的な営みです。

3Rとの出会い

その夜、ユイは社内の技術ブログを漁っていて、1つの記事を見つけた。

ソフトウェア設計の3R

  • Readability(可読性): コードは書く時間より読む時間の方が長い
  • Reusability(再利用性): 同じ問題を何度も解くのは時間の無駄
  • Refactorability(リファクタリング可能性): 変更できないコードは負債になる

記事の著者は、10年前に同じ苦しみを経験したシニアエンジニアだった。記事の冒頭にこんな一節があった。

私が入社した頃、先輩から「良いコードとは何か?」と聞かれました。私は「バグがないコード」と答えました。先輩は首を振りました。「良いコードとは、読んだ人が次の行を予測できるコードだ」と言いました。それから10年、その言葉の意味を噛み締めています。

ユイはその一文を付箋に書いてモニターに貼った。「読んだ人が次の行を予測できるコード」。

# 「次の行を予測できる」コードとはこういうこと
 
# Before: 次に何が来るか予測できない
def p(o, u, c)
  x = Order.find(o)
  if x.st == 1
    if u.pr == true
      x.tt = x.tt * 0.8  # ← 0.8が何なのか予測できない
    end
    # ...
 
# After: 次の行を読む前から意図が伝わる
def process_order(order_id, user, coupon_code)
  order = Order.find(order_id)
  return false unless order.pending?  # ← pending?でなければ何もしない、とわかる
 
  apply_premium_discount(order, user)  # ← 次の処理が予測できる
  apply_coupon(order, coupon_code)     # ← そしてクーポン適用
  complete_order(order, user)          # ← 最後に注文完了
  true
end

process_order を読んだ人は、次の行に「プレミアム割引の適用」が来ると予測できる。そして実際にそう書いてある。これが「読んだ人が次の行を予測できるコード」だ。

プロローグのコードをAfterで見てみる

まだ全部を学んでいないが、ユイは「もしあのコードが3Rで書かれていたら」を想像してみた。

# After: 3Rを適用したコード
class Order < ApplicationRecord
  PREMIUM_DISCOUNT_RATE = 0.20  # 定数で意図を明示
 
  scope :pending, -> { where(status: 'pending') }
 
  def pending?
    status == 'pending'
  end
 
  def apply_premium_discount_for(user)
    return unless user.premium?
    self.total = (total * (1 - PREMIUM_DISCOUNT_RATE)).round(2)
  end
end
 
class Coupon < ApplicationRecord
  def redeemable?
    !used? && expired_at > Time.current
  end
 
  def apply_to(order)
    return false unless redeemable?
    order.total -= discount_amount
    mark_as_used!
    true
  end
 
  def mark_as_used!
    update!(used: true)
  end
end
 
# 注文処理メソッド(意図が伝わる)
def process_order(order_id, user, coupon_code: nil)
  order = Order.find(order_id)
  return false unless order.pending?
 
  order.apply_premium_discount_for(user)
  apply_coupon_if_present(order, coupon_code)
  complete_order(order, user)
  true
end
 
private
 
def apply_coupon_if_present(order, coupon_code)
  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, user)
  order.update!(status: :completed)
  OrderMailer.confirmation(user.email, order.id).deliver_later
end

「完全に理解していなくても、このコードなら読めば何をしているかわかる」ユイは思った。process_order を読むだけで「注文を処理するメソッドで、プレミアム割引とクーポンを適用して注文を完了させる」とわかる。変数名を解読する必要がない。

INFO

ソフトウェアは書かれる時間より読まれる時間の方が圧倒的に長い。Readability(可読性)を高めることは、チーム全体の生産性への投資です。今日の5分の投資が、来月の2時間を節約します。

この旅で学ぶこと

ユイはドキュメントにメモを書き始めた。

# これから学ぶこと
module ThreeRs
  # R1: Readability — 読んで理解できるコードを書く
  #   - 意図が伝わる命名
  #   - 単一責任のメソッド
  #   - Railsの規約を活かす
 
  # R2: Reusability — 同じロジックを安全に使い回す
  #   - DRY原則(Don't Repeat Yourself)
  #   - Service Object
  #   - モジュール・Gem化
 
  # R3: Refactorability — 変更が怖くないコードにする
  #   - SOLID原則
  #   - テスト駆動
  #   - 依存性の注入
end

3つのRは互いに関係している。読みやすいコードは再利用しやすく、再利用しやすいコードはリファクタリングしやすい。そして全部できると、変更が怖くなくなる。

Loading diagram...

旅の地図

この本で歩む道筋を確認しよう。

第1部: Readability(第2〜3章) 命名規則とメソッド分割の基本から始まり、実際のRailsモデル(Fat Model問題)を改善する実践まで。「読んだ人が次の行を予測できる」コードを書けるようになる。

第2部: Reusability(第4〜5章) DRY原則とService Objectで重複をなくす方法、さらに社内Gemやモジュール化でチームレベルの再利用性を実現する。

第3部: Refactorability(第6〜7章) SOLID原則と依存性注入で変更に強い設計を学び、リファクタリングカタログで実際のRailsコードを改善する。

第4部: チームとインフラ(第8〜9章) 3Rの観点でコードレビューを行うスキルと、AWSインフラにも3Rを適用するIaC(Infrastructure as Code)の実践。

エピローグ(第10章) 3ヶ月後のユイが振り返る数値での成果と、3Rを習慣化するための具体的なアドバイス。

3Rが変えるもの

「3Rを実践すると何が変わるんですか?」ユイは田中さんに聞いた。

田中さんはしばらく考えてから答えた。

「コードを書くことへの『誇り』が生まれる。書いた後に『これは良いコードだ』と思えるようになる。そして、変えることへの『恐怖』がなくなる。今日書いたコードを、明日自分でリファクタリングできる。3ヶ月後の新入社員が読んで理解できる。そういうコードを書けるようになる」

ユイは頷いた。あのレガシーコードを書いた人は、きっと誇りを持てなかっただろう。「動けばいい」という妥協から生まれたコードは、誰にも誇りを与えない。

さあ、最初の「R」から始めよう。


付録: レガシーコードの判別基準

3Rの旅を始める前に、「レガシーコード」を認識する目を養おう。

# レガシーコードのサイン(赤信号)
 
# 1. 意味不明な変数名
x = Order.find(o)           # 赤: x, o
order = Order.find(order_id) # 緑: order, order_id
 
# 2. 否定の否定
if !user.not_premium?  # 赤: 二重否定で頭が痛い
if user.premium?       # 緑: ストレートに読める
 
# 3. マジックナンバー
order.total * 0.8     # 赤: 0.8が何かわからない
order.total * (1 - PREMIUM_DISCOUNT_RATE)  # 緑: 意図が明確
 
# 4. 1メソッドに複数の「and」
def create_and_notify_and_log_order   # 赤: andが複数
def create_order                       # 緑: 1つのことだけ
 
# 5. コメントで補うコード
# ユーザーがプレミアムなら割引
if u.pr == true        # 赤: コメントが必要なほど不明瞭
if user.premium?       # 緑: コメント不要で意図が伝わる

これらのサインを見つけたら、それは改善のチャンスだ。一度に全部直そうとしなくてもいい。1つずつ、少しずつ良くしていけばいい。ユイの旅が始まった。

付録: 技術的負債とは何か

3Rを学ぶ前に、「技術的負債(Technical Debt)」という概念を理解しておこう。

技術的負債とは、短期的な利益のために意図的または非意図的に積み重ねられた「将来のコスト」だ。財務の「借金」に例えることができる。

技術的負債の例:

1. 命名の負債
   - 今日: 5分節約(x と書いた)
   - 将来: 毎回2時間の解読作業 × 何十回も

2. テストの負債
   - 今日: 2時間節約(テストを書かなかった)
   - 将来: リファクタリングができない → コードが劣化し続ける

3. 重複コードの負債
   - 今日: 30分節約(既存コードをコピー&ペースト)
   - 将来: 変更のたびに複数箇所を修正 → 1箇所変え忘れてバグ

4. 密結合の負債
   - 今日: 1時間節約(直接依存で書いた)
   - 将来: 変更が連鎖 → 変更が怖い → 開発速度が低下

負債には「利子」がある。放置するほど利子が膨らみ、返済が難しくなる。

# 技術的負債の返済: 計画的に取り組む
# Martin Fowler の「技術的負債の四象限」
 
# 無謀 & 意図的(最悪)
# 「時間がないからテストは書かない」
# → 後で絶対問題になるとわかっているのに選ぶ判断
 
# 慎重 & 意図的(戦略的)
# 「締め切りのために今は動くものを出す。来週リファクタリングする」
# → 意識的な判断。来週本当にリファクタリングすれば問題ない
 
# 無謀 & 非意図的(気づいていない)
# 「レイヤーって何?」
# → 知識不足で負債を作っている。学習で解決
 
# 慎重 & 非意図的(避けられない)
# 「今になってわかった、このアーキテクチャでは対応できない」
# → 事前に知る方法がなかった。受け入れて対処する
 
module TechnicalDebt
  # 返済の優先順位
  # 1. 変更頻度が高い × 複雑度が高い → 最優先
  # 2. バグの温床になっている → 高優先
  # 3. 読むたびに時間を取られる → 中優先
  # 4. 完璧じゃないが機能している → 低優先(触れない)
end

WARNING

技術的負債は「悪いエンジニアが作るもの」ではありません。締め切りプレッシャー、要件の変化、知識の不足——誰もが負債を作る状況に置かれます。重要なのは「負債の存在を認識し、計画的に返済する」ことです。

付録: 3Rを学ぶ前の心構え

「完璧なコードを一度で書こうとしないこと」

これが最も大切な心構えだ。

3Rを学ぶ際の心構え:

✅ 「今日は1つだけ改善する」
   → 完璧主義は行動の敵。小さな一歩が習慣を作る

✅ 「以前のコードを批判しない」
   → 当時はベストを尽くしていた。今の知識で評価するのは不公平

✅ 「コードの問題をシステムで解決する」
   → 個人の注意力ではなく、RuboCop・CI・レビュープロセスで解決する

✅ 「テストはコストではなく投資」
   → テストに使う1時間は、将来の何十時間もの調査時間を節約する

✅ 「3Rは完成しない」
   → コードは常に「より良くなれる状態」にある。旅は続く

❌ 「全部リファクタリングしてから新機能を追加する」
   → リファクタリングは機能追加と並行して少しずつ行う

❌ 「このコードはどうしようもない」
   → どんなコードも、一歩ずつ改善できる

準備はいいか。ユイの旅とともに、あなたの3R旅も始まる。