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_amountx は「何か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
enduser.premium? は「このユーザーはプレミアムですか?」という質問であり、答えは true か false。質問形式にすることで、読み手は「このメソッドは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
endINFO
変数名・メソッド名は「省エネ」に付けてはいけません。あなたが名前を考える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つのメソッドに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
endprocess_order を読むだけで「何をするメソッドか」が一目瞭然になる。各サブメソッドの名前が「ドキュメント」として機能している。
メソッドは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行を超えた巨大モデルの解体と再設計——に挑戦する。