mybook

Readability — 読みやすいコードを書く

「コードは人間のために書く」

翌朝、ユイは田中さんのデスクに向かった。「3Rについて教えてください。まず最初のR、Readabilityから」

田中さんはコーヒーを一口飲んでから言った。「まず最初の質問をしよう。コードは誰のために書くの?」

「コンピュータのため、ですか?」

「違う。コードはコンピュータじゃなく、人間のために書くんだ。コンピュータはどんな書き方でも実行してくれる。アセンブリだろうと、意味不明な変数名だろうと、正しく動く。でも人間は読めなければ理解できない」

田中さんはキーボードを叩いた。

# この2つは、コンピュータにとって同じ意味
def p(o)
  o.tt = o.tt * 0.8
  o.save
end
 
def apply_premium_discount(order)
  order.total = order.total * 0.8
  order.save
end

「コンピュータはどちらも同じように実行する。でも人間にとっては天と地の差がある。下のコードを読んだ人は、1秒で『ああ、プレミアム割引を適用しているんだ』とわかる。上のコードは30秒考えても意味が掴めない」

INFO

コードは書かれる回数より読まれる回数の方が圧倒的に多い。1度書いたコードを、チームメンバーや未来の自分が何十回も読む。読みやすさは最重要の品質指標です。「動くこと」は最低限の条件であって、「読みやすいこと」が本当の品質です。

なぜ可読性が重要なのか: 数字で見るコスト

「感覚的な話だけじゃなく、数字で見てみよう」田中さんが言った。

エンジニアが1日8時間働くとして、その時間をどう使っているかを調べた研究がある。

作業時間の割合
新機能の実装30%
既存コードの読解40%
バグ修正20%
ミーティング・その他10%

「驚くことに、エンジニアの最も多くの時間は『読むこと』に使われている。新しいコードを書く時間より、既存のコードを理解する時間の方が多い。つまり可読性を上げると、チーム全体の生産性を直接改善できる

# 読解時間の比較実験(チームで実施)
# 以下のコードを「理解する」のに何秒かかるか?
 
# テスト1(低可読性)
def c(u, p, d)
  t = p * (1 - d)
  u.bl -= t
  u.save
  Ac.new(u, t, 'p').save
end
 
# テスト2(高可読性)
def charge_user(user, price, discount_rate)
  charged_amount = price * (1 - discount_rate)
  user.balance -= charged_amount
  user.save
  AccountingRecord.new(user, charged_amount, 'payment').save
end
 
# 実際の計測結果
# テスト1: 平均23秒(しかも理解できたか確信が持てない)
# テスト2: 平均3秒(即座に理解できた)
# → 可読性改善で読解速度が7倍以上改善

命名規則: 意図を名前に込める

可読性の最大の武器は「命名」だ。良い名前は、コードを読む人の「考える負荷」を劇的に下げる。

変数名は「何」ではなく「何のために」

変数名には2種類のアプローチがある。「何か(What)」を表す名前と、「何のために(For What)」を表す名前だ。

# Before: 「何」を表す名前(型や構造を説明している)
x = Order.find(params[:id])
t = x.items.sum(&:price)
d = t * 0.1
r = t - d
 
# After: 「何のために」を表す名前(意図・役割を説明している)
order = Order.find(params[:id])
subtotal = order.items.sum(&:price)
discount_amount = subtotal * 0.1
final_price = subtotal - discount_amount

x は「何か1つのもの」を意味するが、「注文」は意味しない。order なら「注文」と明確にわかる。t は「total? temp? time?」と複数の意味に見えるが、subtotal なら「小計」と一義的に理解できる。

名前の長さと読みやすさのバランス

# 短すぎる(意味が失われる)
def calc(u, p)
  u.pt += p * 0.01
end
 
# 長すぎる(冗長で読み疲れる)
def calculate_and_add_loyalty_points_for_user_based_on_purchase_price(user_object, purchase_price_in_yen)
  user_object.total_loyalty_points += purchase_price_in_yen * 0.01
end
 
# ちょうど良い(意図が伝わり、冗長でない)
def add_loyalty_points(user, purchase_price)
  user.loyalty_points += (purchase_price * LOYALTY_RATE).floor
end

名前の長さの目安は「スコープに比例する」。メソッドの外から見える変数(インスタンス変数、クラス変数)は長い名前にし、メソッド内でのみ使うローカル変数は短くても良い。

class OrderProcessor
  # インスタンス変数: クラス全体で使われるので詳細な名前
  def initialize(order, current_user)
    @order_being_processed = order
    @requesting_user = current_user
  end
 
  def call
    # ローカル変数: メソッド内だけなので短くても良い
    items = @order_being_processed.items
    total = items.sum(&:price)
    apply_discounts(total)
  end
end

ブール値は「質問形式」で命名する

ブール値を表すメソッドや変数は、「Yes/No」で答えられる質問の形にする。

# Before: 状態を表す名前(曖昧で質問と答えが混在)
premium = user.plan == 'premium'         # premiumは名詞か形容詞か?
expired = coupon.expired_at < Time.now  # ロジックが逆転している
active_user = user.active                # active_userか active?かわからない
 
# After: Yes/Noで答えられる質問形式(? で終わる)
class User < ApplicationRecord
  def premium?
    plan == 'premium'
  end
 
  def active?
    active && !locked?
  end
 
  def can_place_order?
    active? && !suspended? && verified_email?
  end
end
 
class Coupon < ApplicationRecord
  def expired?
    expired_at <= Time.current
  end
 
  def used?
    used_at.present?
  end
 
  def redeemable?
    !expired? && !used?
  end
end

user.premium? は「このユーザーはプレミアムですか?」という質問であり、答えは truefalse。質問形式にすることで、読み手は「このメソッドはbooleanを返す」と即座にわかる。

コレクションは複数形で

単一のオブジェクトと複数のオブジェクトのコレクションを、名前で区別する。

# Before: 複数のものに単数形(混乱の元)
item = order.items           # itemsを受け取っているのにitem
data = []                    # dataは何のデータ?
result = User.where(active: true)  # resultも曖昧
 
# After: コレクションは複数形、意味のある名前
items = order.items
cart_items = []
active_users = User.where(active: true)
pending_orders = Order.where(status: :pending)
# イテレーションでも単数・複数を使い分ける
active_users.each do |user|      # users(複数) の中の user(単数)
  items.each do |item|           # items(複数) の中の item(単数)
    puts "#{user.name}: #{item.name}"
  end
end

INFO

変数名・メソッド名は「省エネ」に付けてはいけません。あなたが名前を考える1分は、将来そのコードを読む人の何十分もの解読時間を節約します。命名はチームへの投資です。

メソッド分割: 単一責任を徹底する

「何をするか」を語るメソッド名

プロローグのコードを段階的に改善しよう。

# Before: 1つのメソッドがすべてをやる(30行、5つの責任)
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

このメソッドは以下の5つのことをしている:

  1. 注文を取得する
  2. 注文のステータスをチェックする
  3. プレミアム割引を適用する
  4. クーポンを検証して適用する
  5. 注文を完了させてメールを送る

「1つのメソッドに5つの責任がある。これを分解しよう」

# After Step1: メソッド名と変数名を改善し、責任を分割
def process_order(order_id, user, coupon_code)
  order = Order.find(order_id)
  return false unless order.pending?  # ガード節で早期リターン
 
  apply_premium_discount(order, user)
  apply_coupon(order, coupon_code)
  complete_order(order, user)
  true
end

process_order を読むだけで「何をするメソッドか」が一目瞭然になる。各サブメソッドの名前が「ドキュメント」として機能している。

Loading diagram...

メソッドは10行以内を目指す

# After Step2: 各処理を独立したメソッドへ(各メソッドが1つのことだけをする)
 
# プレミアム割引を適用する(単一責任: 割引の適用だけ)
def apply_premium_discount(order, user)
  return unless user.premium?
 
  discount = (order.total * PREMIUM_DISCOUNT_RATE).round(2)
  order.total -= discount
  order.discount_amount = discount
end
 
# クーポンを適用する(単一責任: クーポンの適用だけ)
def apply_coupon(order, coupon_code)
  return if coupon_code.blank?
 
  coupon = Coupon.find_by(code: coupon_code)
  return unless coupon&.redeemable?
 
  order.total -= coupon.discount_amount
  coupon.mark_as_used!
end
 
# 注文を完了させる(単一責任: 完了処理だけ)
def complete_order(order, user)
  order.update!(status: :completed, completed_at: Time.current)
  OrderMailer.confirmation(user.email, order.id).deliver_later
end

各メソッドが5〜8行に収まり、何をしているかが名前と内容から明確にわかる。

ガード節(Guard Clause)で入れ子を減らす

# Before: 深い入れ子(右に行くほど読みにくい)
def process_refund(order, user)
  if order.completed?
    if order.refundable?
      if user.can_refund?
        # やっとここで本処理
        order.update!(status: :refunded)
        RefundMailer.notify(user, order).deliver_later
      end
    end
  end
end
 
# After: ガード節で早期リターン(入れ子が減り、本処理が際立つ)
def process_refund(order, user)
  return unless order.completed?
  return unless order.refundable?
  return unless user.can_refund?
 
  # 本処理だけが残る(視覚的にわかりやすい)
  order.update!(status: :refunded)
  RefundMailer.notify(user, order).deliver_later
end

ガード節のパターンは「条件が満たされなければ早期リターン、満たされれば本処理」という読み方ができる。メソッドの「Happy Path(正常系)」が最後に来ることで、何を達成するメソッドかが明確になる。

Railsの規約: 規約に従うことが可読性

Railsには「設定より規約(Convention over Configuration)」という哲学がある。Railsの規約に従うコードは、Railsを知っている人なら誰でも読めるという強みがある。

スコープで意図を表現する

# Before: コントローラに条件が散らばる(読むたびに条件を解釈する)
def index
  @orders = Order.where(status: 'pending')
                 .where('created_at > ?', 1.week.ago)
                 .order(created_at: :desc)
                 .limit(50)
end
 
def admin_index
  @orders = Order.where(status: 'pending')
                 .where('created_at > ?', 1.week.ago)
                 .order(created_at: :desc)
                 .limit(100)  # adminは100件
end
 
# After: スコープで意図を名前化する(条件を読む必要がない)
class Order < ApplicationRecord
  scope :pending, -> { where(status: 'pending') }
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
  scope :newest_first, -> { order(created_at: :desc) }
end
 
def index
  @orders = Order.pending.recent.newest_first.limit(50)
end
 
def admin_index
  @orders = Order.pending.recent.newest_first.limit(100)
end

スコープ名を読むだけで「最近の未処理注文を新しい順に」という意図が伝わる。SQLの詳細を知らなくてもコードが読める。

モデルのメソッドでビジネスルールを表現する

# Before: ビジネスルールがコントローラに漏れ出る
class OrdersController < ApplicationController
  def create
    # ビジネスルールがコントローラに書かれている
    if current_user.orders.where(status: 'completed').count >= 10 &&
       current_user.registered_at < 1.year.ago &&
       params[:order][:total] > 5000
      discount = params[:order][:total] * 0.05
    end
    # ...
  end
end
 
# After: ビジネスルールはモデルに閉じ込める
class User < ApplicationRecord
  LOYALTY_BONUS_THRESHOLD = 10
  LOYALTY_MEMBER_SINCE = 1.year
  LOYALTY_MINIMUM_ORDER = 5000
  LOYALTY_DISCOUNT_RATE = 0.05
 
  def eligible_for_loyalty_discount?(order_total)
    loyal_customer? && order_total >= LOYALTY_MINIMUM_ORDER
  end
 
  def loyalty_discount_for(order_total)
    return 0 unless eligible_for_loyalty_discount?(order_total)
    (order_total * LOYALTY_DISCOUNT_RATE).round(2)
  end
 
  private
 
  def loyal_customer?
    completed_orders.count >= LOYALTY_BONUS_THRESHOLD &&
      registered_at <= LOYALTY_MEMBER_SINCE.ago
  end
end
 
# コントローラはシンプルに
class OrdersController < ApplicationController
  def create
    discount = current_user.loyalty_discount_for(order_params[:total].to_i)
    # ...
  end
end

ビジネスルール(「10回以上注文した1年以上のユーザーに5%割引」)がモデルに閉じ込められ、コントローラはそのルールを知らなくても良くなった。

コールバックは慎重に使う

# Before: コールバックで副作用が隠れる(可読性を下げる)
class Order < ApplicationRecord
  after_save :send_notification
  after_save :update_inventory
  after_save :record_analytics
 
  private
 
  def send_notification
    # save を呼ぶだけでメールが飛ぶ——驚き(Surpriseの原則に違反)
    OrderMailer.status_changed(user, self).deliver_later if saved_change_to_status?
  end
 
  def update_inventory
    # save を呼ぶたびに在庫が変わる——テストで予測しにくい副作用
    items.each { |item| InventoryService.update(item) }
  end
end
 
# After: 明示的なサービスを使う(副作用が明示される)
class OrderCompletionService
  def call(order)
    ActiveRecord::Base.transaction do
      order.update!(status: :completed)
      notify_user(order)
      update_inventory(order)
      record_analytics(order)
    end
  end
 
  private
 
  def notify_user(order)
    OrderMailer.confirmation(order.user, order).deliver_later
  end
 
  def update_inventory(order)
    order.items.each { |item| InventoryService.decrease(item) }
  end
 
  def record_analytics(order)
    AnalyticsService.track('order.completed', order_id: order.id)
  end
end
 
# 使用側: 何が起きるかが明示的
OrderCompletionService.new.call(order)

WARNING

Railsのコールバックは便利ですが、過度に使うと「どこで何が起きているか」が追えなくなります。save を呼んだら意図せずメールが飛んだ——こういう「驚き」は可読性の大敵です。重要な副作用は明示的なサービスで表現しましょう。「最小驚き の原則(Principle of Least Surprise)」を守ることが可読性の柱です。

コメント: 「なぜ」だけを書く

良いコードはコメントなしで読める。コメントは「なぜ(Why)」のためだけに使う。

# Bad: コードの繰り返し(What を説明——コードが読めれば不要)
# ユーザーを見つける
user = User.find(id)
 
# Bad: 廃れやすいコメント(コードが変わってもコメントが残る)
# 2023年3月1日 田中が追加: プレミアムユーザーへの割引
def apply_discount(user, order)
  # ...
end
 
# Good: なぜこの実装にしたかを説明(Why を説明——コードからは読み取れない)
# MySQLのロック競合を避けるためにfind_by(id:)ではなくfind()を使う
# SELECTにFOR UPDATE ロックが不要なケースではfindが高速
# 詳細: https://github.com/your-org/app/issues/1234
user = User.find(id)
 
# Good: ビジネスルールの背景を説明(なぜ30日なのか)
# 30日はキャンセルポリシー規約(Section 3.2)に基づく
# 変更時は legal@example.com に確認が必要
CANCELLABLE_PERIOD = 30.days
 
# Good: 非直感的な実装の理由を説明
# Rubyのfloat精度問題を避けるためにBigDecimalを使う
# 0.1 + 0.2 == 0.3 が false になる問題への対策
total = items.sum { |item| BigDecimal(item.price.to_s) }

「コメントが必要なコードは、コメントで説明するより名前を改善すべきことが多い」田中さんが言った。「コメントを書く前に、まず『コードで意図を表現できないか』を考えよう」

読みやすさを妨げる「アンチパターン」

# アンチパターン1: 否定の否定(二重否定)
unless !user.inactive?  # 赤: 頭が痛い
if user.active?         # 緑: ストレートに読める
 
# アンチパターン2: 三項演算子の乱用
result = condition1 ? (condition2 ? a : b) : (condition3 ? c : d)  # 赤
# ↓
if condition1
  result = condition2 ? a : b
else
  result = condition3 ? c : d
end  # 緑: 少し長いが読める
 
# アンチパターン3: 意味のない抽象(名前が具体的すぎず抽象的すぎる)
def handle(item)   # 赤: handleは何でもできる意味で何も言っていない
def process(data)  # 赤: processもdataも曖昧すぎる
 
def validate_cart_item(item)   # 緑: 何をするか明確
def calculate_shipping_fee(order)  # 緑: 何を計算するか明確
 
# アンチパターン4: 出力変数が不明確
def calculate(items, users, config)  # 赤: 何を返すかわからない
  # ...
end
 
def calculate_total_price(items, discount_config)  # 緑: 総価格を計算することが明確
  # ...
end

ユイの気づき: 可読性は思いやり

「なるほど」ユイはノートに書き留めた。

可読性チェックリスト:
□ 変数名・メソッド名を読むだけで意図が伝わるか
□ ブール値のメソッドは ? で終わっているか
□ 1つのメソッドが1つのことだけをしているか
□ メソッドは10行以内か(例外的に長くなる場合は理由があるか)
□ 入れ子はガード節で減らせているか
□ Railsのスコープで複雑な条件を名前化しているか
□ コメントは「なぜ」だけを書いているか
□ コレクションは複数形になっているか
□ マジックナンバーは定数に切り出されているか

田中さんが付け加えた。「最高のテストは、コードを初めて見た人が5分で理解できるかどうかだよ。でもそれは技術的なテストじゃなく、思いやりのテストだ。そのコードを読む次の人(それは3ヶ月後の自分かもしれない)への思いやり。可読性は思いやりから生まれる」

「可読性は思いやり」——ユイはそれをメモした。抽象的に聞こえるけれど、実はとても具体的だ。「この変数名を見た人が30秒悩まずに済むように」「このメソッドを読んだ人が次の行を予測できるように」——そういう具体的な「相手への配慮」の積み重ねが、可読性の高いコードを生む。

# 「思いやり」のあるコードとは
# 次の人(または3ヶ月後の自分)が読んだとき:
# - 変数名を解読する必要がない
# - 「これは何をするメソッドか」を推測する必要がない
# - 「この0.8は何だ?」と悩む必要がない
# - コードを頭の中でトレースしなくても流れがわかる
 
# そのために:
# 良い命名 = 解読作業をなくす
# メソッド分割 = 構造を見せる
# ガード節 = 条件を整理する
# スコープ = ビジネス意図を名前にする

次の章では、このプロローグのコードを実際にRailsモデルで改善していく。特に「Fat Model」問題——1000行を超えた巨大モデルの解体と再設計——に挑戦する。