「支払い方法を1つ追加するだけなのに、なぜこんなに時間がかかるのですか?」
前回の記事 コードスメル #4 の続きです。
企画側からよく出る質問です。開発者にとっても答えにくい質問です。
実際に直す場所が12か所あるからです。enumへのケース追加、5か所のswitchの修正、アイコンのマッピングテーブル、文字列定数、分析イベント名、サーバーパラメータ変換、テストフィクスチャです。
各修正は2、3行で難しくありません。難しいのは、12か所を漏れなく見つけることです。
Martin Fowlerは『リファクタリング』で、この状況に名前を付けています。ショットガン手術(Shotgun Surgery)です(著者による原文)。
名前が正確な理由
ショットガンを撃つと、散弾が広く飛び散ります。1つの変更が、コードベースのあちこちに小さな修正として現れるという意味です。
傷は深くありませんが数が多く、1つでも見落とすと問題になります。
本当の問題は見落としたときに起きます。12か所中11か所を直してデプロイすると、残りの1か所が特定の条件でだけ発覚します。
支払い画面は動くのに、領収書画面だけで「不明な支払い方法」と表示される、といった具合です。ショットガン手術のコストは修正時間ではなく、漏れの確率です。
鏡像となるスメルがある
同じ一覧には正反対のスメルもあります。発散的変更(Divergent Change、翻訳によっては「絡み合った変更」)です。
- 発散的変更:1つのモジュールが、互いに異なる複数の理由で頻繁に変更されます。データベースを変更してもこのファイルを直し、支払いルールを変更してもこのファイルを直します。
- ショットガン手術:1つの理由による変更が複数のモジュールに影響します(Shotgun Surgeryの原文)。
2つの関係を図にすると、軸が反転しています。
| 発散的変更 | ショットガン手術 | |
|---|---|---|
| 関心事とコード | 1か所に複数の関心事 | 1つの関心事が複数の場所に |
| 症状 | このファイルが何度も変わる | この変更が何度も広がる |
| 処方 | 分ける | 集める |
処方が正反対であることが重要です。コードスメルの話は「分割しよう」に流れがちですが、ショットガン手術への答えは集約です。
何が問題かを誤ると、処方は正反対の方向に進みます。
この2つを分ける軸は1つです。変更理由です。
単一責任原則を「1つのことだけをする」ではなく「変更理由が1つである」と定義する理由がここにあります。仕事の数は人によって異なりますが、変更理由は実際のコミット履歴で確認できます。
コミット履歴で実測する
感覚ではなくデータを使えます。ショットガン手術は、いつも一緒に変更されるファイルとして現れます。
これを変更結合(change coupling)、または論理的結合と呼びます。
import関係としては見えない結合であることが核心です。2つのファイルが互いを参照していないのに常に同じコミットに現れるなら、コードに書かれていないルールが2つを結び付けています。
最近のコミットで一緒に変更されたファイルのペアを数えると、候補が見つかります。
git log --format='%H' --since=6.months.ago | while read c; do
git show --format= --name-only "$c" | grep '\.swift$' | sort | \
awk 'NR==FNR{a[NR]=$0;n=NR} END{for(i=1;i<n;i++)for(j=i+1;j<=n;j++)print a[i]" + "a[j]}'
done | sort | uniq -c | sort -rn | head -20
上位に来たペアが本当に一緒にあるべきコードかを確認します。
もちろん、実装とそのテストファイルのように、自然に一緒に変わるペアもあります。
それらを除いて残ったものが調査対象です。
集める方法
**分散した条件分岐を1つの型に集めます。**最も一般的な形です。
// 前:支払い方法を知る処理 switchがプロジェクトのあちこちにある
func iconName(for method: PaymentMethod) -> String {
switch method {
case .card: return "creditcard"
case .transfer: return "building.columns"
}
}
func displayName(for method: PaymentMethod) -> String { ... }
func serverCode(for method: PaymentMethod) -> String { ... }
この3つの関数が別々のファイルにあると、支払い方法を追加するたびに3か所を探す必要があります。1つの型に集めれば、1か所になります。
// 後:1か所だけ見ればよい
extension PaymentMethod {
var iconName: String { ... }
var displayName: String { ... }
var serverCode: String { ... }
}
漏れの検出をコンパイラに任せます。 Swiftではswitchがすべてのケースを処理する必要があります。
enumにケースを追加すると、defaultがないすべてのswitchでコンパイルエラーになります。ショットガン手術をなくすことはできませんが、漏れを実行時からコンパイル時へ移せます。
先ほど述べた本当のコストは漏れの確率だったので、これだけでも性質が変わります。
そのため、enumを扱うswitchにdefault: breakを習慣的に追加してはいけません。自分でこの安全網を断つことになるからです。
**文字列キーを型に変えます。**分析イベント名、ユーザーデフォルトのキー、通知名が文字列リテラルとして散在していると、コンパイラは助けてくれません。
定数として1か所に集めるか、型で包めば、追加・変更の場所が1つになります。
**設定をデータにします。**支払い方法ごとにアイコン・名前・コードが必要なら、コードではなく構造体の配列1つで定義できます。
新しい方法は、配列に項目を1つ追加するだけです。
**集めにくければ目印を残します。**物理的に集められない場合もあります。
サーバーとクライアントで同じルールが必要な場合などです。
その場合は、各場所に互いを指すコメントを付けるか、値がずれたら失敗するテストを置きます。人の記憶に頼るしかないなら、少なくとも覚えておく場所を書いておくわけです。
分けることと集めることの間
ここで前回までの記事につながります。コードスメルへの処方はおおむね分けるか集めるかの2方向ですが、どちらかをやりすぎると別のスメルになります。
- 発散的変更を直そうと分ける → ラビオリコード
- ショットガン手術を直そうと集める → ゴッドオブジェクト
- 階層に分ける → ラザニアコード
だから目標を断片の大きさや数にしてはいけません。目標は1つです。
**一緒に変わるものは一緒に置き、別々に変わるものは別々に置く。**凝集度と結合度が最終的に言っているのもこのことです。
判断に迷ったときに使える質問があります。「このコードを最後に変更した理由は何だったか」を3回ほど思い出してみることです。
理由が毎回違ったなら分ける番で、毎回同じ理由で複数の場所を直していたなら集める番です。
まとめ
- ショットガン手術とは、1つの変更が複数ファイルの小さな修正に分散している状態です。
- 本当のコストは修正時間ではなく、漏れの確率です。
- 正反対のスメルである発散的変更は、1つのファイルが複数の理由で変わる状態で、処方も正反対です。一方は集め、もう一方は分けます。
- コミット履歴から常に一緒に変わるファイルのペアを抽出すれば、候補を実測できます。
- Swiftではenumと
switchの網羅チェックによって、漏れをコンパイルエラーに変えられます。defaultを習慣的に追加しないことが、その安全網を守る方法です。
次回は、これらすべてのスメルを一言で呼ぶようになった比喩を扱います。技術的負債です。
Ward Cunninghamが語った本来の意味は「汚いコード」ではありませんでした。
出典と確認基準
- Refactoring — Martin Fowler · 著者による原文 · 確認 2026-08-17 · 根拠:Shotgun Surgery・Divergent Changeのコードスメルとリファクタリング原則
- The Shotgun Surgery Problem — Martin Fowler · 著者による原文 · 確認 2026-08-17 · 根拠:変更が複数モジュールに分散するショットガン手術の事例

![[コードスメル #5] ショットガン手術 vs 発散的変更のカバー画像](/assets/images/posts/d4a472ee-9fec-4ab5-9b61-0e9875b3dae5/shotgun-surgery-scattered-changes.jpg)