0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【リファクタリング】第17回 散弾銃のような変更(Shotgun Surgery)

Posted at

はじめに

散弾銃のような変更(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 箇所に集める」

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?