リファクタリング

目次

概要

振る舞いを変えずに、構造を変える

リファクタリングは、機能追加でも最適化でもなく、外から見える振る舞いを保ったまま内部構造を改善する作業です。目的は「きれいにすること」ではなく、次の変更を安全にしやすくすることにあります。

要点

リファクタリングは未来の変更コストを下げる投資です。テストで振る舞いを守りながら、小さな手順で進めます。

この章で重視すること

  • コードスメルを変更コストの兆候として捉える
  • 大きく書き直すのではなく、小さく段階的に変える
  • リファクタリング前後で安全網としてテストを使う

コードスメル

代表的な兆候には次があります。

  • 長すぎる関数
  • 役割の多すぎるクラス
  • 深い条件分岐
  • 重複
  • 不明瞭な名前
  • 外部依存に強く結びついたロジック

スメルは即バグではありませんが、理解や変更の負担を増やします。

スメルを見る視点

コードスメルは「汚いコード一覧」ではありません。変更するときに余計な場所まで触る必要がある、読んでも責務が分からない、テストが書きにくい、という兆候です。

たとえば長い関数が常に悪いわけではありません。しかし、そこに複数の抽象度の処理が混ざっていたり、条件分岐が業務ルールを隠していたりするなら、分ける価値があります。

進め方

  1. 現在の振る舞いをテストで固定する
  2. 小さな変換を1つだけ行う
  3. テストを通す
  4. 次の変換へ進む

大きな書き換えは、改善と不具合混入の境界が見えにくくなります。

機能追加と構造改善を完全に分けられない場面もあります。その場合でも、コミットやPRの単位では「振る舞いを変えない変更」と「振る舞いを変える変更」をできるだけ分けます。レビューする側が安全に追える粒度にすることも、リファクタリングの技術です。

準備リファクタリング

機能追加の前に、少しだけ構造を整えることがあります。これを準備リファクタリングとして考えると、作業の目的が明確になります。

例:

  • 条件式に名前を付ける
  • 重複した分岐をまとめる
  • 外部API呼び出しを境界へ寄せる
  • テストしやすい純粋関数を切り出す

先に構造を整えることで、本来の変更が小さくなります。

よく使う手筋

  • 変数名・関数名の改善
  • 関数抽出
  • 条件分岐の分離
  • データ構造の整理
  • 依存の注入
  • 巨大なif/elseの置き換え

関数抽出

関数抽出は最も基本的なリファクタリングです。抽出する基準は「行数」ではなく、名前を付けられる概念があるかどうかです。

抽出前:

if (user.plan === "pro" && user.expiresAt > now && !user.suspended) {
  allowExport();
}

抽出後:

if (canExport(user, now)) {
  allowExport();
}

名前が付くと、条件の意味が読みやすくなります。

条件分岐の整理

分岐が深い場合は、早期return、guard clause、strategy、polymorphismなどを検討します。

ただし、設計パターンへ飛びつく必要はありません。まずは次を試します。

  • 否定条件を減らす
  • guard clauseで異常系を先に出す
  • 条件式へ名前を付ける
  • 同じ条件で分岐している箇所を集める

データ構造を変える

多くの複雑さは、処理ではなくデータの形から来ます。flagが多すぎる、nullが多すぎる、文字列で状態を表している、などは分岐を増やします。

状態を型やenum、値オブジェクトへ寄せると、あり得ない状態を減らせます。

リファクタリングと性能

リファクタリングは性能改善ではありません。ただし、構造が整理されると性能問題を測りやすくなることがあります。性能を目的にするなら、必ず計測を先に置きます。

技術的負債は「汚いコード」という感想ではなく、将来の変更速度に利息として効いてくる設計上の借りです。すべての負債をすぐ返す必要はありませんが、場所、理由、返済条件を明示しない負債は、あとで意思決定できない負債になります。

大きな変更を小さく進める

大規模な置き換えでは、完全に作り直すより、既存機能を動かしたまま少しずつ経路を変える方が安全です。

flowchart LR Old["旧実装"] Facade["Facade / 境界"] New["新実装"] Caller["呼び出し元"] Caller --> Facade Facade --> Old Facade -.段階的に移行.-> New

Branch by Abstractionでは、呼び出し元と実装の間に抽象境界を作り、旧実装と新実装を切り替えられるようにします。これにより、長期間の巨大ブランチを避け、mainlineへ小さく統合できます。

安全に進める順序

  1. 現在の振る舞いをテストやログで固定する
  2. 呼び出し元と実装の間に境界を作る
  3. 旧実装を境界の後ろへ移す
  4. 新実装を並べて作る
  5. 小さな範囲から切り替える
  6. 観測して問題なければ範囲を広げる
  7. 旧実装を削除する

この進め方は地味ですが、本番で動いているシステムを止めずに変えるときに強いです。

レビューで見るポイント

  • 変更前後で振る舞いが変わっていないか
  • 差分が小さいか
  • 名前が概念を説明しているか
  • テストが安全網として十分か
  • 依存方向が悪化していないか

リファクタリングの単位

Martin Fowlerは、リファクタリングを小さな振る舞い保存の変換として説明しています。重要なのは、大きな設計変更を一気に行うのではなく、常に動く状態を保ちながら少しずつ形を変えることです。

よい単位:

  • 関数を抽出する
  • 変数名を変える
  • 条件式を説明変数へ出す
  • 重複した式をまとめる
  • classを分割する前に責務を観察する

悪い単位:

  • 動作変更と構造変更を同じcommitに混ぜる
  • テストなしで広範囲に移動する
  • 先に抽象化を作り、後から合わせる

テストを先に置く

安全なリファクタリングでは、変更前の振る舞いを固定します。

  • characterization testを書く
  • 既存bugを仕様として固定しないよう注意する
  • 変更前後で同じ入力を比較する
  • public APIの互換性を確認する
  • 性能が重要ならbenchmarkも保存する

テストが薄い領域では、まず観測と小さなテストを足してから動かします。

抽象化による分岐

大きな置き換えでは、古い実装と新しい実装を一定期間共存させることがあります。これをbranch by abstractionとして扱えます。

flowchart LR Caller["呼び出し側"] --> Interface["抽象境界"] Interface --> Old["旧実装"] Interface --> New["新実装"]

この方法では、切り替え点を小さくし、段階的に新実装へ移せます。ただし抽象境界が長く残ると複雑さになるため、移行完了後に古い実装と一時的な抽象を消すことが重要です。

レガシーコードでの進め方

レガシーコードでは、設計の理想形より先に「安全に触れる場所」を作ります。

  1. 変更したい振る舞いを観察する
  2. 入力と出力を固定する
  3. 変更しやすい継ぎ目を見つける
  4. 小さく抽出する
  5. 変更を加える
  6. 不要になった分岐やadapterを消す

DBや外部APIに強く依存している場合は、まず境界を作ってテスト可能な範囲を増やします。

リファクタリングしない判断

Fowlerの説明でも、リファクタリングは振る舞いを保った小さな変換を積み重ねる活動です。つまり、目的は「きれいにすること」ではなく、変更を安く安全にすることです。今後ほとんど触らないコードや、変更範囲が明確でないコードを大きく整理すると、費用だけが増えることがあります。

リファクタリングを見送る判断もあります。

状況 判断
もう変更しないlegacy 触る理由が出るまで維持する
仕様理解が浅い 先にcharacterization testを書く
障害対応中 最小修正を優先し、後で整理する
抽象化の方向が不明 重複を観察してから分ける
性能上の制約が強い profileしてから構造を変える

よいリファクタリングは、次の変更を簡単にします。逆に、次の変更が分からないまま構造だけを先回りすると、将来の自分に余計な抽象を渡すことになります。

どこから返済するか

技術的負債は、ただ古いコードや読みにくいコードを指す言葉ではありません。Fowlerの技術的負債の説明では、内部品質の不足によって将来の変更に余分なコストが乗る状態として扱われます。つまり、返済の優先順位は「汚さ」だけでなく、「今後どれだけ触るか」「触るたびにどれだけ遅くなるか」で決めます。

quadrantChart title リファクタリング優先度 x-axis 変更頻度が低い --> 変更頻度が高い y-axis 変更コストが低い --> 変更コストが高い quadrant-1 すぐ返済する quadrant-2 監視しながら計画する quadrant-3 放置してよいことが多い quadrant-4 ついでに直す "料金計算": [0.86, 0.82] "古い管理画面": [0.25, 0.72] "小さな重複": [0.72, 0.22] "停止予定機能": [0.15, 0.18]
状況 判断
変更頻度が高く、変更が毎回遅い 機能追加の前に準備リファクタリングを入れる
障害が多く、理解に時間がかかる characterization testと観測を先に足す
ほとんど触らないが読みにくい 無理に直さず、触る理由が出るまで待つ
大規模置換が必要 branch by abstractionやfeature flagで段階移行する
性能が問題 リファクタリング前にprofileと基準値を取る

コードスメルは入口であって、優先順位そのものではありません。長い関数、重複、巨大クラスを見つけたら、まず「次の変更で利息を払う場所か」を確認します。よく変わる場所の小さな改善は、たいてい大きな一括改修より安全で効果が出やすいです。

リファクタリングパターンの具体的な実装例

Extract Method(メソッド抽出)の段階的実行

複雑な関数から明確な責務を持つ小さな関数を抽出する最重要パターン。

Before: 長いメソッド(計算量 O(n^2) の原因):

def calculate_order_total(order):
    subtotal = 0
    tax_rate = 0.08
    shipping_cost = 10.0
    
    # 計算ロジック混在
    for item in order.items:
        if item.category == "electronics":
            subtotal += item.price * 0.95  # 5% discount
        elif item.category == "books":
            subtotal += item.price * 1.0
        else:
            subtotal += item.price * 0.9
    
    # 状態追跡
    if subtotal > 100:
        shipping_cost = 0
    elif subtotal > 50:
        shipping_cost = 5
    
    tax = subtotal * tax_rate
    total = subtotal + tax + shipping_cost
    
    # 複数の責務
    if total > 1000:
        print(f"Large order warning: ${total}")
    
    return total

After: Extract Method で 4 つの関数に分割:

def calculate_order_total(order):
    subtotal = calculate_subtotal(order)
    tax = calculate_tax(subtotal)
    shipping = calculate_shipping(subtotal)
    return subtotal + tax + shipping

def calculate_subtotal(order):
    return sum(apply_category_discount(item) for item in order.items)

def apply_category_discount(item):
    discounts = {
        "electronics": 0.95,
        "books": 1.0,
        "other": 0.9
    }
    rate = discounts.get(item.category, 1.0)
    return item.price * rate

def calculate_tax(subtotal):
    return subtotal * 0.08

def calculate_shipping(subtotal):
    if subtotal > 100:
        return 0.0
    elif subtotal > 50:
        return 5.0
    return 10.0

効果:

  • 各関数の McCabe cyclomatic complexity: 3-5 → 1-2
  • テスト可能性: 単一責務で各部をユニットテスト可能
  • 再利用性: 各関数が独立して他のコンテキストで使用可能

条件分岐をポリモーフィズムに置き換える

条件分岐(if-else の深いネスト)をポリモーフィズムで置き換える。

Before: 型チェックとキャストが散在:

class PaymentProcessor:
    def process(self, payment, amount):
        if payment.type == "credit_card":
            card = payment.details
            # クレジットカード処理
            response = submit_to_gateway(card.number, card.cvv, amount)
            if not response.success:
                raise PaymentError(response.error)
        elif payment.type == "paypal":
            paypal_id = payment.details
            # PayPal 処理
            response = call_paypal_api(paypal_id, amount)
            if response.status != "completed":
                raise PaymentError("PayPal failed")
        elif payment.type == "bank_transfer":
            account = payment.details
            # 銀行振込処理
            if not validate_account(account.number):
                raise PaymentError("Invalid account")
            record_transfer(account.number, amount)
        else:
            raise ValueError(f"Unknown type: {payment.type}")

After: Strategy パターンでポリモーフィズム:

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def process(self, amount):
        pass

class CreditCardPayment(PaymentStrategy):
    def __init__(self, card):
        self.card = card
    
    def process(self, amount):
        response = submit_to_gateway(self.card.number, self.card.cvv, amount)
        if not response.success:
            raise PaymentError(response.error)
        return response

class PayPalPayment(PaymentStrategy):
    def __init__(self, paypal_id):
        self.paypal_id = paypal_id
    
    def process(self, amount):
        response = call_paypal_api(self.paypal_id, amount)
        if response.status != "completed":
            raise PaymentError("PayPal failed")
        return response

class BankTransferPayment(PaymentStrategy):
    def __init__(self, account):
        self.account = account
    
    def process(self, amount):
        if not validate_account(self.account.number):
            raise PaymentError("Invalid account")
        record_transfer(self.account.number, amount)
        return {"status": "recorded"}

class PaymentProcessor:
    def __init__(self, strategy: PaymentStrategy):
        self.strategy = strategy
    
    def process(self, amount):
        return self.strategy.process(amount)

# 使用
processor = PaymentProcessor(CreditCardPayment(card_obj))
processor.process(100.0)

利点:

  • 各決済方法の追加/変更が独立
  • 新規決済方法の追加時に既存コードの修正不要(Open/Closed Principle)
  • テスト: 各 Strategy を独立してテスト可能

パラメータオブジェクトの導入(パラメータオブジェクト導入)

複数の関連パラメータを 1 つのオブジェクトで統一。

Before: パラメータが散在:

def create_user(name, email, age, phone, address, city, zip_code):
    # 7つのパラメータの管理が複雑
    if age < 18:
        raise ValueError("Must be 18+")
    if not validate_email(email):
        raise ValueError("Invalid email")
    # ...
    return User(name, email, age, phone, address, city, zip_code)

# 呼び出しが複雑で間違いやすい
user = create_user("John", "john@ex.com", 30, "555-1234", "123 Main", "NYC", "10001")

After: オブジェクトで統一:

from dataclasses import dataclass

@dataclass
class UserProfile:
    name: str
    email: str
    age: int
    phone: str
    address: str
    city: str
    zip_code: str
    
    def validate(self):
        if self.age < 18:
            raise ValueError("Must be 18+")
        if not validate_email(self.email):
            raise ValueError("Invalid email")
        if not validate_phone(self.phone):
            raise ValueError("Invalid phone")

def create_user(profile: UserProfile):
    profile.validate()
    return User(profile)

# 呼び出しが明確
profile = UserProfile(
    name="John",
    email="john@ex.com",
    age=30,
    phone="555-1234",
    address="123 Main",
    city="NYC",
    zip_code="10001"
)
user = create_user(profile)

効果:

  • 関連パラメータを概念的にグループ化
  • 検証ロジックが 1 箇所に集中
  • 新規パラメータ追加が容易
  • 関数シグニチャが安定(パラメータ数の変化が減少)

重複コードの統合

DRY 原則に従い、重複したコードロジックを 1 箇所に集約。

Before: 重複したバリデーション:

class UserService:
    def create_user(self, data):
        if not data.get("name"):
            raise ValueError("Name required")
        if not data.get("email"):
            raise ValueError("Email required")
        if len(data.get("name", "")) < 2:
            raise ValueError("Name too short")
        # ...
        return User.create(data)
    
    def update_user(self, user_id, data):
        if not data.get("name"):
            raise ValueError("Name required")
        if not data.get("email"):
            raise ValueError("Email required")
        if len(data.get("name", "")) < 2:
            raise ValueError("Name too short")
        # ...
        return User.update(user_id, data)
    
    def import_users(self, csv_data):
        for row in csv_data:
            if not row.get("name"):
                raise ValueError("Name required")
            if not row.get("email"):
                raise ValueError("Email required")
            if len(row.get("name", "")) < 2:
                raise ValueError("Name too short")
            # ...
            User.create(row)

After: 共通バリデータ:

class UserValidator:
    @staticmethod
    def validate(data):
        errors = []
        if not data.get("name"):
            errors.append("Name required")
        elif len(data["name"]) < 2:
            errors.append("Name too short")
        
        if not data.get("email"):
            errors.append("Email required")
        
        if errors:
            raise ValueError("; ".join(errors))
        return True

class UserService:
    def create_user(self, data):
        UserValidator.validate(data)
        return User.create(data)
    
    def update_user(self, user_id, data):
        UserValidator.validate(data)
        return User.update(user_id, data)
    
    def import_users(self, csv_data):
        for row in csv_data:
            UserValidator.validate(row)
            User.create(row)

計算量への影響:

  • 重複排除により保守コストが O(n) → O(1)(修正点 1 箇所)
  • バグ修正の伝播が確実

リファクタリングのアンチパターン

過度なリファクタリング

変更されない部分に対する過度な最適化/抽象化。

症状:

  • 1 度だけ使用される Helper クラス
  • 5 行のコードを 3 つのメソッドに分割
  • 単一責務原則の過解釈(関数が細かすぎて理解困難)

対策:

  • リファクタリングは “pain を感じたら” 実行(YAGNI: You Aren’t Gonna Need It)
  • 3 回同じパターンが現れたら初めて抽象化を検討

時期尚早な抽象化

十分な情報がないまま抽象化を進める。

症状:

# Bad: 1 つの実装しかないのに Abstract Base Class
class PaymentStrategyBase(ABC):
    @abstractmethod
    def pay(self):
        pass

class CreditCardPayment(PaymentStrategyBase):
    def pay(self):
        # ...

対策: 抽象化は 2-3 の具体的な実装が存在して初めて効果的

不適切なリファクタリング

リファクタリングと機能追加を同時に実行(テスト困難化)。

Bad:

# リファクタリング + 新機能を同時にコミット
def calculate_total(items):
    # リファクタリング: サブルーチン抽出
    subtotal = calculate_subtotal(items)
    # 新機能: ポイント機能追加
    points = subtotal * 0.01
    return subtotal + calculate_tax(subtotal) + points

Good: 分離

# コミット 1: リファクタリングのみ
def calculate_total(items):
    subtotal = calculate_subtotal(items)
    tax = calculate_tax(subtotal)
    return subtotal + tax

# コミット 2: ポイント機能を追加
def calculate_total(items):
    subtotal = calculate_subtotal(items)
    tax = calculate_tax(subtotal)
    points = calculate_points(subtotal)
    return subtotal + tax + points

リファクタリング パターン集

Martin Fowler のカタログでは50以上のリファクタリング手法が記録されています。実務で頻出の10パターンを把握すれば、大部分のコード改善が対応できます。

Extract Method(メソッド抽出)

長いメソッドから一まとまりのロジックを独立したメソッドに切り出す、最もよく使うリファクタリング。

# Before
def calculate_total():
    subtotal = sum(prices)
    tax = subtotal * 0.1
    return subtotal + tax

# After
def calculate_total():
    subtotal = sum(prices)
    return subtotal + calculate_tax(subtotal)

def calculate_tax(amount):
    return amount * 0.1

利点:再利用性、テスト容易性、可読性の向上。

変数名・メソッド名の変更

意図が不明な変数名や関数名を、その役割を明確に反映した名前に変更。

# Before
def calc(p, q):
    r = p * q
    return r * 0.8

# After
def calculate_discounted_price(base_price, quantity):
    subtotal = base_price * quantity
    return subtotal * 0.8

パラメータオブジェクトの導入

複数の関数が同じグループのパラメータを繰り返し受け取っている場合、それを1つのオブジェクトにまとめる。

デッドコードの削除

使用されていないコード(変数、関数、クラス)を削除。Version Control がある場合は、安全に削除可能。

クラス抽出

1つのクラスが複数の責務を持つ場合、責務ごとに分割。

マジックナンバーを名前付き定数に置き換える

意味不明な数値リテラルを、明確な名前を持つ定数に置き換え。

// Before
if (age > 18 && salary > 50000) { /* ... */ }

// After
const int ADULT_AGE = 18;
const int MIN_QUALIFYING_SALARY = 50000;
if (age > ADULT_AGE && salary > MIN_QUALIFYING_SALARY) { /* ... */ }

リファクタリング の計測と優先順位

「あらゆるコードをリファクタリングする」のではなく、変更頻度複雑度 の高い部分から優先します。

コード複雑度メトリクス

  • Cyclomatic Complexity: if/for/while/case の分岐数。10 を超えたら分割候補。
  • Lines of Code (LOC): 1メソッド 20-30行を目安に。
  • Fan-In / Fan-Out: クラスが呼び出される数と、クラスが他を呼び出す数。

準備的リファクタリングの考え方

Fowler は「新機能追加の 前に リファクタリングする」ことを推奨。

リファクタリングの手法分類

関数レベルの変換

メソッド抽出(Extract Method): 大きなメソッドから、意味のある部分を新しいメソッドに切り出す。

// 前
public void printReportHeader() {
    System.out.println("Report: " + reportName);
    System.out.println("Date: " + new Date());
    System.out.println("Author: " + author);
    System.out.println("---");
}

// 後
public void printReportHeader() {
    printTitle();
    printMetadata();
    printSeparator();
}

private void printMetadata() {
    System.out.println("Date: " + new Date());
    System.out.println("Author: " + author);
}

インライン化(Inline Method): 単純すぎるメソッドを呼び出し元に展開。逆操作。

# 前
def get_price(order):
    return order.quantity() * order.item_price()

# 後: 呼び出し元で直接計算
total = order.quantity() * order.item_price()

変数の改名: スコープの狭い順に実行。引数→ローカル変数→フィールド→メソッド。

クラス・メソッドレベル

クラスの責務分離: 1つのクラスが複数の役割を持つ場合、Strategy パターン等で分解。

SRP(Single Responsibility Principle)違反の検出:

  • “と” で繋がる責務が複数ある
  • 変更理由が複数ある
  • テストのセットアップが複雑

Move Method と Extract Class: 関連するメソッド・フィールドを別クラスへ。

# 例:Customer クラスから計算ロジックを Payment クラスへ
# 前
class Customer:
    def get_payment_due(self, invoice):
        if self.is_premium:
            return invoice.amount * 0.9
        else:
            return invoice.amount

# 後
class PaymentCalculator:
    def get_payment_due(self, customer, invoice):
        return invoice.amount * self.get_discount_rate(customer)

    def get_discount_rate(self, customer):
        return 0.9 if customer.is_premium else 1.0

条件分岐の整理

Guard Clause(早期リターン): 多重ネストの if を扁平化。

// 悪い例
public void process(Order order) {
    if (order != null) {
        if (order.isValid()) {
            if (order.isPaid()) {
                // 処理
            }
        }
    }
}

// 良い例
public void process(Order order) {
    if (order == null) return;
    if (!order.isValid()) return;
    if (!order.isPaid()) return;
    // 処理
}

Polymorphism による置き換え: 条件分岐を Strategy/State パターンで多態化。

# 前:大きな if-elif
def apply_discount(customer):
    if customer.type == "premium":
        return price * 0.9
    elif customer.type == "vip":
        return price * 0.8
    else:
        return price

# 後:多態
class DiscountStrategy:
    def calculate(self, price): raise NotImplementedError

class PremiumDiscount(DiscountStrategy):
    def calculate(self, price): return price * 0.9

class VIPDiscount(DiscountStrategy):
    def calculate(self, price): return price * 0.8

# 使用側
discount = strategy_for(customer)
result = discount.calculate(price)

データ構造の変換

Replace Temp with Query: 計算結果をメンバ変数やメソッドに変える。

Replace Array with Object: インデックスベースのアクセスを名前付きフィールドに。

# 前
person = ["Alice", 30, "Engineer"]
puts person[0]  # "Alice" (意図不明)

# 後
class Person
  attr_accessor :name, :age, :occupation
end

person = Person.new
puts person.name  # 明確

Introduce Parameter Object: 複数の関連パラメータをオブジェクトにまとめる。

// 前
function drawChart(width, height, backgroundColor, fontColor, title) { }
drawChart(800, 600, "#fff", "#000", "Sales");

// 後
interface ChartConfig {
  dimensions: { width: number; height: number };
  colors: { background: string; font: string };
  title: string;
}

function drawChart(config: ChartConfig) { }
drawChart({
  dimensions: { width: 800, height: 600 },
  colors: { background: "#fff", font: "#000" },
  title: "Sales"
});

レガシーコード のリファクタリング

テストがない状況での対応

Sprout Method(新芽法): 新しくテストしやすい純粋関数を抽出し、汚い部分は最小化。

// 既存の複雑なコード
public class Processor {
    public void ProcessAndSave(DataSet ds) {
        // 複雑な計算
        int result = ds.Values.Sum() * 1.1;
        
        // DB 保存
        db.Save(result);
    }
}

// Sprout する
public class Processor {
    public void ProcessAndSave(DataSet ds) {
        int result = CalculateTotal(ds);  // <-- 新しい pure メソッド
        db.Save(result);
    }
    
    // テスト可能
    public int CalculateTotal(DataSet ds) {
        return ds.Values.Sum() * 1.1;
    }
}

Wrap Method(ラッピング): 既存メソッドをラッパーで包み、新しい動作をそこに追加。

レガシーコードのテスト化

Seams Model: テストしやすい接合部(seam)を見つけ、そこで依存を切る。

# 前:DB に強く依存
class OrderProcessor:
    def process(self, order_id):
        order = db.fetch(order_id)  # <-- Seam
        # 複雑な処理
        return result

# 後:Seam を通じて Mock 注入
def test_process():
    mock_db = Mock()
    processor = OrderProcessor(db=mock_db)
    processor.process(1)
    mock_db.fetch.assert_called_once()

大規模リファクタリング戦略

抽象化による分岐

段階的な置き換え:

  1. 新しい抽象層を導入(インターフェース)
  2. 既存実装と新実装を両方サポート
  3. 徐々に新実装へ移行
  4. 古い実装を削除
// 段階 1: インターフェース定義
type PaymentProvider interface {
    Process(amount float64) error
}

// 段階 2: 既存実装をラップ
type LegacyPayment struct {}
func (l *LegacyPayment) Process(amount float64) error {
    return legacyPaymentSystem(amount)  // 古い実装
}

// 新しい実装
type StripePayment struct {}
func (s *StripePayment) Process(amount float64) error {
    return stripeAPI.Charge(amount)
}

// 段階 3: どちらを使うかを選択可能
func ProcessPayment(provider PaymentProvider, amount float64) error {
    return provider.Process(amount)
}

ストランゲラー・フィグパターン

新しいコードが古いシステムを包み込み、徐々に置き換える(マーチン・ファウラーが推奨)。

パフォーマンスと リファクタリング

プロファイリング先行

リファクタリングで性能が低下する可能性:

  1. コストの無視:処理回数が増えるリファクタリングは避ける

    • 例:毎回オブジェクト生成する純粋関数化
  2. プリコンパイル と キャッシュ

    // 前
    Pattern p = Pattern.compile(regex);  // 毎回
    
    // 後
    private static final Pattern p = Pattern.compile(regex);
    
  3. O(n) リファクタリングに注意

    # 悪い例
    def find_user(users, id):
        for user in users:  # O(n)
            if user.id == id:
                return user
    
    # リファクタリング前に index を作るべき
    users_by_id = {u.id: u for u in users}
    user = users_by_id[id]  # O(1)
    

測定ドリブン リファクタリング

  1. ベースライン測定
  2. リファクタリング前後での性能比較
  3. アクセプタンス基準の設定

リファクタリングの品質指標

メトリクス

メトリクス 改善の目標
Cyclomatic Complexity < 10
関数行数 < 50
クラス行数 < 300
重複行数 < 3%
テストカバレッジ > 80%

コードレビューでのチェック項目

  1. 一度の PR は「振る舞いを変えない」か「機能を追加」かのどちらか
  2. インデント深度の増加がないか
  3. 新しく導入される依存は最小か
  4. テストが追加されているか
変更したい機能 → まずリファクタリング → 次に新機能 → テスト

例えば、「ユーザー登録画面に2段階認証を加える」場合:

  1. 現在の認証ロジックを別クラスに抽出 (Preparatory Refactoring)
  2. 2段階認証ロジックを新クラスとして追加 (新機能)
  3. 統合テストで両者を検証

リファクタリングと技術負債

技術負債(Technical Debt)とは、「今は楽だが、将来のコスト増になる設計上の選択」。

種別 原因 対策
重複コード Copy-paste、抽象化の遅延 Extract Method、Introduce Parameter Object
不明瞭な名前 急いで書いた Rename Variable、Extract Method
大きすぎるメソッド/クラス 責務の混在 Extract Method、Extract Class
密結合 依存関係の明確化不足 Introduce Parameter Object、Dependency Injection
テストなし リファクタリングの心理的障壁 テストファースト導入

Fowler は「技術負債は利息を払い続ける状態」と述べます。定期的なリファクタリングで、その利息を減らすことが大切。

リファクタリングとチーム文化

効果的なリファクタリングは、単なるコードの問題ではなく、チームの信頼と規律に依存します。

  • Code Review: リファクタリングの提案・レビューを通じた学習
  • Pair Programming: 一緒にリファクタリングすることで、判断基準を共有
  • Definition of Done: リファクタリングを完了条件に含める
  • Legacy Code へのアプローチ: Michael Feathers 『Working Effectively with Legacy Code』の手法(characterization test、seams model)

リファクタリングは「あとで困らないようにする」ための基本動作です。コードの見た目より、変更しやすさ、理解しやすさ、局所的な検証のしやすさを基準にすると判断しやすくなります。

まとめ

参考文献

講義・記事

書籍

解説・補助