プロローグ — コードの「匂い」に気づく日
入社3ヶ月目の朝
ケンタは画面を見つめながら、コーヒーを一口飲んだ。
東京・渋谷のオフィス。窓の外には秋晴れが広がっているが、そんな景色に目を向ける余裕はなかった。先週から担当している「配送料計算機能」のプルリクエストに、シニアエンジニアの山田さんからコメントが届いていた。
「動くには動くけど、このコード、少し『匂い』がするね。」
匂い?コードに匂いなんてあるのか……
ケンタは入社してから3ヶ月、毎日一生懸命コードを書いてきた。大学ではアルゴリズムの授業をそれなりに頑張ったし、個人開発でいくつかのWebアプリも作った経験がある。でも、「匂い」という言葉は聞いたことがなかった。
ケンタが書いたコードはこんな感じだった。
class OrderController < ApplicationController
def calculate_shipping
order = Order.find(params[:id])
if order.user.premium?
if order.total_price > 10000
shipping_fee = 0
else
shipping_fee = 300
end
elsif order.user.regular?
if order.total_price > 5000
shipping_fee = 500
else
shipping_fee = 800
end
else
if order.destination == "hokkaido" || order.destination == "okinawa"
shipping_fee = 1500
else
shipping_fee = 1000
end
end
render json: { shipping_fee: shipping_fee }
end
end「動くし、テストも全部グリーンだ。何が問題なんだろう?」
ケンタはSlackで山田さんに返信した。「動作確認もしました。テストも通っています。何が問題でしょうか?」
数分後、返信が来た。「ちょっと話そう。5分だけいい?」
コードの匂いとは何か
山田さんはケンタのデスクに来て、静かに椅子を引いて隣に座った。
「ケンタくん、このコードは今は動く。でも3ヶ月後、『法人会員』という新しい会員種別が追加されたらどうする?」
ケンタは考えた。「……条件分岐を追加すれば……」
「そう。またこのコントローラを開いて、ifを増やすことになる。半年後には?1年後には?」
山田さんはキーボードを借りて、半年後の予想図をさらさらと書いた。
# 半年後のコードの予想図
if order.user.premium?
# プレミアム会員の計算ロジック
# ...(20行)
elsif order.user.regular?
# 一般会員の計算ロジック
# ...(20行)
elsif order.user.corporate? # 新規追加:法人会員
# 法人会員の計算ロジック
# ...(30行)
elsif order.user.student? # 新規追加:学生会員
# 学生向けの割引ロジック
# ...(15行)
elsif order.user.staff? # 新規追加:社員
# 社員割引のロジック
# ...(10行)
else
# ゲスト向けの標準ロジック
# ...(20行)
end「このコードを1年後に触ることになった後輩が何人いるかわからない状態で、あなたがこのコードを触るとしたら、どう感じる?」
ケンタは少し考えた。「……怖いです。どこか変更したら、他の条件に影響が出るかもしれないから。」
「そう。それがコードの匂いだ。」
山田さんは穏やかに続けた。「コードの匂いというのは、設計上の問題を示す兆候のこと。今は動くけど、将来の変更を困難にする可能性がある。臭いが強ければ強いほど、後で痛い目に遭う可能性が高い。」
「このコードには複数の問題がある」と山田さんは続けた。
まず、変更するたびに既存コードを壊すリスクが高い。if-elseの海の中に手を入れるとき、どこかの条件を壊してしまう可能性がある。しかも、そのバグが気づかれないまま本番に出ることもある。
次に、テストが書きにくい。これはコントローラのテストなのか、計算ロジックのテストなのか、境界が不明確だ。計算ロジックを変えるたびにコントローラのテストを修正しないといけない。
最後に、コードの意図が読み取りにくい。「何が変わっているのか」「変わらないのはどこか」が、長い条件分岐の中に埋もれている。
INFO
コードの「匂い」(Code Smell)とは、設計上の問題を示唆する兆候のこと。Martin Fowlerの著書『リファクタリング』で有名になった概念。今は動いていても、将来の変更を困難にする可能性がある。代表的な匂いとして「長すぎるメソッド」「巨大なクラス」「霰弾銃的変更」「依存関係の連鎖」などがある。
「なぜ」を理解すること
ケンタは率直に聞いた。「じゃあ、最初からどう書けばよかったんですか?」
山田さんはゆっくり答えた。「それを今すぐ教えることはできる。でも、先に『なぜ問題なのか』を完全に理解してほしい。解決策を覚えるより、問題を理解するほうが大事だから。」
「理解しました。問題はコントローラが計算ロジックを全部知っていることですよね?」
「そう。もっと正確に言うと、変化の速度が違うものが同じ場所にあるんだ。コントローラの処理フロー(リクエストを受けてレスポンスを返す)はほとんど変わらない。でも計算ロジック(どの会員種別にいくら請求するか)は、ビジネスの都合でどんどん変わる。速度が違うものを一緒に置くと、遅く変わるものまで毎回触らなければならなくなる。」
ケンタは「なるほど」とノートに書き留めた。
パターンという先人の知恵
「ソフトウェア設計の世界には、同じような問題を何度も解いてきた先人たちがいる。彼らが『この種の問題にはこう対処するといい』と名前をつけてまとめたものが デザインパターン だ。」
1994年、GoF(Gang of Four)と呼ばれる4人の著者——Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides——が、23のパターンを体系化した書籍「Design Patterns: Elements of Reusable Object-Oriented Software」を発表した。30年以上経った今でも、ソフトウェア業界で最も影響力のある本の一つだ。
「でも全部覚えなくていい」と山田さんは付け加えた。「パターンは目的じゃない。よいコードを書くための共通言語 だ。チーム全員が『ここはStrategyパターンを使おう』と言えれば、設計の議論が5分で終わる。それが10分、20分の議論になっていたところが。」
「共通言語か……」ケンタは繰り返した。「レシピみたいなものですか?」
「いい例えだ」と山田さんは笑った。「レシピを知っている料理人は、毎回ゼロから考えなくて済む。でもレシピは素材や状況によって変えるものだ。パターンも同じで、状況に合わせて適用するものだ。」
デザインパターンが解決する3つの問題
1. 変更に強いコード
要件は必ず変わる。ビジネスが成長すれば新しい会員種別が増え、新しい決済方法が追加され、新しい通知チャネルが必要になる。パターンを使うと、「変わる部分」と「変わらない部分」を明確に分離できる。
レゴブロックの比喩で考えてほしい。今のコードは粘土で作られた一体成型のオブジェクトだ。一部を変えたければ、全体を作り直さないといけない。パターンを適用すると、レゴブロックのように各部分が交換可能になる。
2. チームの共通言語
プロジェクトに新しいメンバーが加わったとき、「ここはStrategyパターンで実装されています」と言えるだけで、そのコードの構造と意図が瞬時に伝わる。
「この処理は通知が来たら複数の箇所に伝えないといけない」→「Observerパターンだね」。こういう会話ができると、設計の意図が素早く共有できる。長いドキュメントを読まなくても、パターン名一つで概念が共有される。
3. 有名な問題に対する実績ある解法
何千ものプロジェクトで試され、洗練されてきたアプローチ。「こういう状況でこのパターンを使ったら失敗した」という知見も蓄積されている。ゼロから考えるより確実で、先人の失敗から学べる。
WARNING
パターンは「銀の弾丸」ではない。すべての問題にパターンを当てはめようとする「パターン強迫」は、かえってコードを複雑にする。シンプルな問題にはシンプルな解決策が最善だ。まず問題を深く理解し、パターンが本当に必要かを判断することが重要。「パターンを知っているから使う」ではなく、「この問題にはこのパターンが有効だ」という判断が大事。
SOLID原則との関係
デザインパターンを学ぶ前に、SOLID原則を知っておくと理解が深まる。パターンはSOLIDの具体的な実装例とも言える。
| 原則 | 意味 | パターンへの関連 |
|---|---|---|
| S: 単一責任 | クラスは1つの責任のみ | Strategyで計算を分離 |
| O: 開放/閉鎖 | 拡張に開き、修正に閉じる | Factoryで追加を容易に |
| L: リスコフ置換 | 派生クラスは基底クラスと交換可能 | Strategyの戦略交換 |
| I: インターフェース分離 | 必要なインターフェースのみ依存 | Observer の疎結合 |
| D: 依存関係逆転 | 抽象に依存し、具象に依存しない | Factoryで生成を分離 |
「SOLIDって教科書で見たことあります」とケンタは言った。「でも、具体的にどういうことかはあまりわかっていませんでした。」
「各パターンを学べば、自然とSOLIDが体に染み込む。抽象的な原則より、具体的なコードで学ぶほうが早い。」
Railsとデザインパターン
実は、Railsのフレームワーク自体がデザインパターンの塊だ。ケンタはすでにパターンを使っていた——名前を知らなかっただけだ。
| Railsの機能 | 使われているパターン | 説明 |
|---|---|---|
ActiveRecord | Active Record パターン | DB行をオブジェクトに対応させる |
before_action | Chain of Responsibility | リクエスト処理を連鎖させる |
ActiveSupport::Notifications | Observer パターン | イベントを購読者に通知する |
Rails.application.config | Singleton パターン | 設定オブジェクトを1つに保つ |
| Middleware stack | Decorator パターン | 処理に機能を積み重ねる |
ActiveRecord::Base.create | Template Method | 共通の生成フローを定義する |
has_many :through | Proxy パターン | 中間テーブルを透過的に扱う |
「Railsを使っている時点で、ケンタくんはすでにパターンを使いこなしていた。ただ、意識していなかっただけだよ。」
ケンタは少し誇らしい気持ちになった。そして同時に、もっと深く理解したいと思った。
AWSインフラにもパターンがある
ソフトウェアだけじゃない。インフラにも同じ考え方が適用される。これを知ると、パターンが「コードの書き方」に留まらない普遍的な概念だとわかる。
CloudFront はProxyパターンだ。ユーザーはオリジン(EC2やS3)を意識せずに、CloudFrontを通してコンテンツを受け取る。オリジンが変わっても、ユーザーへのインターフェース(URL)は変わらない。
Auto Scaling Group はFactoryパターンだ。「このLaunch Templateに基づいてインスタンスを作れ」と定義しておくと、需要に応じてEC2インスタンスを自動生成する。誰がどうやって作るかは意識しなくていい。
SNS + SQS + Lambda の組み合わせはObserverパターンだ。SNSにメッセージが来たら(Subject)、サブスクライブしているLambda(Observer)に通知が届く。新しいLambdaを追加しても、SNSのコードを変える必要はない。
ALB(Application Load Balancer) はChain of Responsibilityパターンだ。リクエストをリスナーのルールに基づいて順番に評価し、適切なターゲットグループに振り分ける。
INFO
インフラとアプリケーションコードが同じパターンで設計されていると、チーム全体での設計議論が統一された言語で行えるようになる。「このマイクロサービスの連携はObserverパターンになっているね」という会話がインフラエンジニアとアプリエンジニアの間でできるようになる。
コードの匂いの代表的なパターン
山田さんはコードの匂いにはいくつかの典型があると教えてくれた。これを知っておくと、「何かおかしい」という直感を言語化できるようになる。
長すぎるメソッド(Long Method)
1つのメソッドが30行、50行、100行と膨らんでいく。「ここだけ読めばこの機能がわかる」という良さがある一方、テストしにくく、変更しにくい。ケンタのコントローラがまさにこれだ。
# 匂いのあるコード:1つのアクションで何十行もの処理
def complete_order
order = Order.find(params[:id])
order.update!(status: :completed)
# 在庫更新(10行)
# ...
# メール送信(5行)
# ...
# ポイント計算(8行)
# ...
# Slack通知(5行)
# ...
# ダッシュボード更新(7行)
# ...
render json: { status: "ok" }
end
# 合計:50行以上のアクション巨大なクラス(God Class)
何でもできるクラス。ユーザー管理も、注文処理も、在庫管理も、メール送信も全部できる UserManager クラスなど。1つのクラスが多くの責務を持ちすぎている。
重複コード(Duplicated Code)
コピー&ペーストでコードが増殖する。バグを修正するとき、同じコードを10か所修正しなければならない。修正漏れでバグが残る。
# コントローラAで
fee = order.total_price > 5000 ? 500 : 800
# コントローラBで(コピーしたもの)
fee = order.total_price > 5000 ? 500 : 800
# サービスクラスで(またコピー)
fee = order.total_price > 5000 ? 500 : 800
# → 閾値が5000から3000に変わったら3か所全部直す必要がある霰弾銃的変更(Shotgun Surgery)
1つの変更が何十ものファイルに散らばって影響する。「消費税率を8%から10%に変更する」だけで、10ファイルを修正しなければならない状態。
密結合(Tight Coupling)
クラスAがクラスBの内部実装を知りすぎている。Bを変えると必ずAを変えなければならない。
# 密結合の例:コントローラがモデルの内部を知りすぎている
def show
@user = User.find(params[:id])
# コントローラがUserの内部フィールドを直接参照
@display_name = "#{@user.first_name} #{@user.last_name}"
# → Userの名前フィールド設計が変わったら、このコントローラも変える必要がある
end
# 疎結合の例:モデルに表示用メソッドを持たせる
def show
@user = User.find(params[:id])
@display_name = @user.display_name # モデルが知っている
endINFO
コードの匂いを嗅ぐ力は経験とともに育つ。最初は「なんか気持ち悪い」という直感で十分だ。その直感を言語化できるようになると、コードレビューで建設的なフィードバックができるようになる。「このコードは○○という匂いがするので、△△パターンで解決できます」という言語化がゴールだ。
コードレビューで指摘されること
ケンタはこの日から、コードレビューの視点が変わった。レビューコメントに「なぜそう書くのか」の背景が見えるようになってきた。
よくあるレビューコメントと、その背景にある設計思想:
| レビューコメント | 背景の設計思想 | 対応するパターン |
|---|---|---|
| 「コントローラが太りすぎ」 | 単一責任原則 | Service Object |
| 「条件分岐を減らせないか」 | 開放/閉鎖原則 | Strategy |
| 「テストが書きにくそう」 | テスト可能性 | 依存性注入 |
| 「変更の影響範囲が広い」 | 疎結合 | Observer |
| 「オブジェクト生成が散らばっている」 | 責務の集中 | Factory |
| 「この処理、他でも使いそう」 | DRY原則 | Template Method |
「最初はレビューコメントの意図がわからなかった」とケンタは日記に書いた。「でも、設計原則とパターンを知ると、レビューアーが何を心配しているのかが見えてくる。」
パターンを学ぶ姿勢
「パターンを覚えようとする人がよく失敗する」と山田さんは言った。「名前と構造だけ覚えて、どこでも使おうとする。」
「正しい姿勢は?」
「問題から始めることだ。『このコードはなぜ変更が難しいのか』を考える。その答えを出してから、それに対応するパターンを探す。そうすると、パターンが自然と見えてくる。」
「パターンが解決する問題を理解するってことですか?」
「そう。ケンタくんは今日、自分のコードに問題があることを理解した。これがスタートだ。次は、その問題をStrategyパターンで解決する方法を見ていこう。」
リファクタリングの第一歩
パターンを適用するとき、一気にリファクタリングしようとすると失敗する。山田さんはリファクタリングの手順を教えてくれた。
1. まずテストを書く
テストがないと、リファクタリングが「壊していない」という確信が持てない。最初にテストを書くことで、安全な変更が可能になる。
# まず現在の動作をテストで確認する
RSpec.describe OrdersController do
describe "GET #calculate_shipping" do
let(:premium_user) { create(:user, membership_type: "premium") }
let(:order) { create(:order, user: premium_user, total_price: 15_000) }
it "プレミアム会員の10000円超は送料無料" do
get :calculate_shipping, params: { id: order.id }
expect(JSON.parse(response.body)["shipping_fee"]).to eq(0)
end
end
end2. 小さいステップで変える
一度に全部変えない。1つのクラスを切り出し、テストがグリーンのままであることを確認し、次に進む。
3. コードが悪い状態で新機能を追加しない
仕様変更の前に、まず設計を改善する。「ボーイスカウトルール」——コードを触るたびに、触る前より少しだけ良くして去る。
WARNING
リファクタリングは「動作を変えずにコードを改善すること」だ。リファクタリング中に新機能を追加しようとすると、バグの原因がリファクタリングなのか新機能なのかわからなくなる。「リファクタリングをする」「機能を追加する」は別のコミットで行う。
これからの旅
この本では、ケンタの成長物語を通じて15のパターンを学んでいく。
各章の構成はシンプルだ:
- ケンタが直面する「日常の問題」——誰もが経験するリアルな状況
- パターンを使わない「問題のあるコード」——なぜ問題かを詳しく分析
- パターンを適用した「改善されたコード」——Before/Afterの比較
- AWSインフラでの「同じ考え方」——インフラとの共通点
- テストの書き方——パターンがテストをどう簡単にするか
パターンは一度学べば終わりではない。実際にコードを書いて、失敗して、直して、ようやく体に染み込む。この本はそのガイドブックだ。
山田さんの最後のひとことが印象的だった。
「パターンを覚えるんじゃなくて、パターンが解決する問題 を理解するんだよ。そうすれば自然と使えるようになる。あと、パターンを使わなくていい場面を見極める目も養ってほしい。」
ケンタはノートを開き、「なぜデザインパターンが必要か」と書いた。
そしてその下に、今日の気づきを書き加えた。
コードは書いた瞬間から「過去のコード」になる。次に触れるのは自分かもしれないし、チームメイトかもしれないし、1年後の新入社員かもしれない。その人が迷わないコードを書くことが、プロのエンジニアの仕事だ。デザインパターンは、そのための先人の知恵だ。
次章では、ケンタが山田さんのアドバイスを受けながら、最初のパターン「Strategy」を学んでいく。あの忌まわしい条件分岐地獄が、どう変わるのかを見ていこう。
INFO
この章のまとめ
- コードの「匂い」は動くが変更が困難なコードの兆候で、将来の苦しみの予兆
- デザインパターンは先人が体系化した「よく使う解決策」の名前付きカタログ
- GoFの23パターンは生成・構造・振る舞いの3カテゴリに分類される
- Railsのフレームワーク自体がパターンの実装例(ActiveRecord、Notifications、Middleware等)
- AWSインフラ(CloudFront、Auto Scaling、SNS)にも同じパターンが見られる
- パターンは目的ではなく、よいコードを書くための手段——問題を理解してから適用する
- SOLID原則はパターンを支える設計思想であり、パターンを学ぶと自然に身につく