はじめに
散弾銃のような変更(Shotgun Surgery) とは、
1つの小さな変更を行うのに、あちこちの複数のクラス・メソッドを同時に修正しなければならない状態 を指します。
例:
- 顧客の住所仕様が変わったとき、UI / DB / バリデーション / レポート出力などに散在する住所処理を全部直す必要がある
- 新しいログ出力ルールを導入するのに、すべてのクラスの
printlnを探して修正しなければならない
これは 「関心事の分散」 が原因で発生します。
17.1 特徴
- 同じ意味の修正 が複数の場所に必要になる
- 「この変更はどこを直せばいいのか?」が分散している
- 一貫性を保つのが難しく、修正漏れのバグ を招きやすい
- 発散的変更(Divergent Change)の「逆パターン」と言える
17.2 解決手法
-
クラス統合(Move Method / Move Field / Extract Class)
→ 散らばったロジックを 1 箇所にまとめる -
Observer パターン / イベント駆動
→ 「複数の箇所に同じ変更を入れる」代わりに、通知で自動反映 -
Decorator / Middleware パターン
→ 共通処理を横断的にまとめる -
集中化(Centralize Responsibility)
→ ロギング / バリデーション / フォーマット処理は専用クラスに集約
17.3 Kotlin 例
Before:散弾銃的変更
class Order {
fun printInvoice() {
println("=== Invoice ===")
// ... 明細表示
println("=== End ===")
}
}
class Report {
fun printReport() {
println("=== Report ===")
// ... 集計表示
println("=== End ===")
}
}
class Logger {
fun log(message: String) {
println("[LOG] $message")
}
}
- それぞれのクラスで「ヘッダーやフッターの出力形式」を持っており、
出力形式の変更 が必要になると全部のクラスを修正する羽目になる。
After①:共通ユーティリティに集約
object Printer {
fun printWithHeaderFooter(title: String, block: () -> Unit) {
println("=== $title ===")
block()
println("=== End ===")
}
}
class Order {
fun printInvoice() {
Printer.printWithHeaderFooter("Invoice") {
// ... 明細表示
}
}
}
class Report {
fun printReport() {
Printer.printWithHeaderFooter("Report") {
// ... 集計表示
}
}
}
→ 出力形式を 1 箇所に集約。変更は Printer だけで済む。
After②:Observer パターンで分散更新を回避
interface EventListener {
fun onEvent(event: String)
}
class EventBus {
private val listeners = mutableListOf<EventListener>()
fun subscribe(listener: EventListener) = listeners.add(listener)
fun publish(event: String) = listeners.forEach { it.onEvent(event) }
}
class AuditLogger : EventListener {
override fun onEvent(event: String) {
println("[AUDIT] $event")
}
}
class NotificationService : EventListener {
override fun onEvent(event: String) {
println("[NOTIFY] $event")
}
}
→ 「ログ出力」「通知」などをイベントに分離し、
1 箇所の publish で全体に反映。
17.4 実務での指針
- 同じ修正が複数箇所で必要なら 共通化できないか? を疑う
- 共通処理は 専用クラス・ライブラリ・ミドルウェア にまとめる
- 横断的関心事(ログ / 監査 / フォーマット)は 集中管理
- Kotlin / Spring なら AOP や Interceptor、Flutter/Android なら Middleware/Delegate を検討
まとめ
- Shotgun Surgery は「小さな変更のはずが、あちこち直さないといけない」悪臭
- 解決策は 集約・共通化・イベント駆動化
- 基本思想:「関心ごとを分散させず、1 箇所に集める」