Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

リファクタリング

リファクタリングとは

  • 定義:外部から見た振る舞いを変えずに、内部構造を改善すること

  • 目的:可読性・変更容易性を高め、バグを見つけやすくする

いつリファクタリングするか

  • 3度目の法則(Rule of Three):3回同じようなことをしたら抽象化を考える

  • 準備のためのリファクタリング:「まず変更を簡単にし、それから簡単になった変更をする」(Kent Beck)

  • 理解のためのリファクタリング:コードを読んで理解した内容を、名前や構造としてコードに反映する

  • ボーイスカウトルール:触ったコードは来たときより少しきれいにして帰る(機会主義的に日常作業へ織り込む)

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

    • 修正する必要がなく、外から触れることもないコード

    • 書き直した方が早い場合

進め方の原則

  • 小さなステップで進め、常にコンパイル・テストが通る状態を保つ

  • テストという安全網(自己テストコード)を先に整える

    • テストがない場合の戦略:接合部(Seam)、スプラウトメソッド(『レガシーコード改善ガイド』)

  • 振る舞いの変更と構造の変更をコミットで分ける(『Tidy First?』)

コードの臭い(Code Smells)

  • 重複コード

  • 長い関数

  • 長いパラメータリスト

  • 変更の偏り(Divergent Change):1つのモジュールが複数の理由で頻繁に変更される

  • 散弾銃手術(Shotgun Surgery):1つの変更のために多数のモジュールを少しずつ修正する必要がある

  • 特性への横恋慕(Feature Envy):自分のモジュールより他のモジュールのデータばかり触っている

  • データの群れ:いつも一緒に現れるデータの集まり → クラスにまとめる候補

  • 基本データ型への執着:金額・座標・範囲などを表す小さなクラスを作らず、プリミティブ型で済ませてしまう

  • コメントは消臭剤:コメントで補いたくなったら、まず構造を疑う

リファクタリング・カタログ

使用頻度の高いものから。before / after のコード例をメモしていく。

関数の抽出(Extract Function)/関数のインライン化

意図(何をするか)と実装(どうやるか)を分離する。名前を付けられる単位で抽出する。逆操作がインライン化。

# --- Before: コメントで区切られた長い関数 ---
def print_owing(invoice):
    outstanding = 0

    # バナーの表示
    print("***********************")
    print("**** Customer Owes ****")
    print("***********************")

    # 未払い金額の計算
    for order in invoice["orders"]:
        outstanding += order["amount"]

    # 明細の表示
    print(f"name: {invoice['customer']}")
    print(f"amount: {outstanding}")
# --- After: コメントを書きたくなった箇所が抽出の合図 ---
def print_owing(invoice):
    print_banner()
    outstanding = calculate_outstanding(invoice)
    print_details(invoice, outstanding)


def print_banner():
    print("***********************")
    print("**** Customer Owes ****")
    print("***********************")


def calculate_outstanding(invoice):
    return sum(order["amount"] for order in invoice["orders"])


def print_details(invoice, outstanding):
    print(f"name: {invoice['customer']}")
    print(f"amount: {outstanding}")

変数の抽出/名前の変更

複雑な式に名前を付けて意図を表す。

# --- Before: 式の意味をコメントで説明している ---
def price(order):
    # 価格 = 基本価格 - 数量割引 + 送料
    return (order.quantity * order.item_price
            - max(0, order.quantity - 500) * order.item_price * 0.05
            + min(order.quantity * order.item_price * 0.1, 100))
# --- After: 名前がコメントの代わりになる ---
def price(order):
    base_price = order.quantity * order.item_price
    quantity_discount = max(0, order.quantity - 500) * order.item_price * 0.05
    shipping = min(base_price * 0.1, 100)
    return base_price - quantity_discount + shipping

関数宣言の変更/パラメータオブジェクトの導入

引数の追加・削除・名前変更。いつも一緒に渡されるデータの群れはオブジェクトにまとめる。

from dataclasses import dataclass
from datetime import date


# --- Before: start_date, end_date がいつも一緒に現れる(データの群れ) ---
def amount_invoiced(start_date, end_date): ...
def amount_received(start_date, end_date): ...
def amount_overdue(start_date, end_date): ...
# --- After: パラメータオブジェクトにまとめる ---
@dataclass(frozen=True)
class DateRange:
    start: date
    end: date

    def contains(self, d: date) -> bool:
        return self.start <= d <= self.end


def amount_invoiced(period: DateRange): ...
def amount_received(period: DateRange): ...
def amount_overdue(period: DateRange): ...

# クラスになると contains() のような振る舞いの置き場所もできる

条件記述の分解/ガード節による入れ子の置き換え

複雑な条件式を関数に抽出する。異常系は早期リターンで抜け、正常系のフローを平坦にする。

# --- Before: 条件も分岐先も式のままで、意図が読み取りにくい ---
def pay_amount(employee):
    if employee.is_separated:
        result = {"amount": 0, "reason": "SEP"}
    else:
        if employee.is_retired:
            result = {"amount": 0, "reason": "RET"}
        else:
            result = compute_normal_pay(employee)  # 本題のロジック
    return result
# --- After: ガード節。異常系は早期リターンで抜け、正常系を平坦に ---
def pay_amount(employee):
    if employee.is_separated:
        return {"amount": 0, "reason": "SEP"}
    if employee.is_retired:
        return {"amount": 0, "reason": "RET"}
    return compute_normal_pay(employee)


# --- 条件記述の分解: 条件式そのものにも名前を付ける ---
# Before
def charge(a_date, quantity, plan):
    if a_date < plan.summer_start or a_date > plan.summer_end:
        return quantity * plan.winter_rate + plan.winter_service_charge
    return quantity * plan.summer_rate

# After
def charge(a_date, quantity, plan):
    if is_summer(a_date, plan):
        return summer_charge(quantity, plan)
    return winter_charge(quantity, plan)

ポリモーフィズムによる条件記述の置き換え

型による switch / if の分岐をクラス階層(またはディスパッチ)に置き換える。

# --- Before: 型で分岐する if の連鎖。種類が増えるたびに全分岐を修正 ---
def speed(bird):
    if bird["type"] == "EuropeanSwallow":
        return 35
    elif bird["type"] == "AfricanSwallow":
        return 40 - 2 * bird["number_of_coconuts"]
    elif bird["type"] == "NorwegianBlueParrot":
        return 0 if bird["is_nailed"] else 10 + bird["voltage"] / 10
    else:
        raise ValueError(bird["type"])
# --- After: 分岐の追加が「クラスの追加」に変わる(既存コードを触らない) ---
class EuropeanSwallow:
    def speed(self):
        return 35


class AfricanSwallow:
    def __init__(self, number_of_coconuts):
        self.number_of_coconuts = number_of_coconuts

    def speed(self):
        return 40 - 2 * self.number_of_coconuts


class NorwegianBlueParrot:
    def __init__(self, voltage, is_nailed):
        self.voltage = voltage
        self.is_nailed = is_nailed

    def speed(self):
        return 0 if self.is_nailed else 10 + self.voltage / 10

ループの分離/パイプラインによるループの置き換え

1つのループで複数のことをしない。内包表記や map / filter への置き換え。

# --- Before: 1つのループで2つの集計をしている ---
def summary(people):
    total_age = 0
    total_salary = 0
    for p in people:
        total_age += p.age
        total_salary += p.salary
    return total_age, total_salary
# --- After: ループを分離すると、それぞれを関数として抽出できる ---
def summary(people):
    total_age = sum(p.age for p in people)
    total_salary = sum(p.salary for p in people)
    return total_age, total_salary


# --- パイプラインによるループの置き換え ---
# Before
names = []
for office in offices:
    if office["country"] == "Japan":
        names.append(office["name"])

# After
names = [office["name"] for office in offices if office["country"] == "Japan"]

問い合わせと更新の分離(コマンド・クエリ分離)

値を返す関数は観察可能な副作用を持たないようにする。

# --- Before: 値を返しつつ副作用もある(問い合わせのつもりで呼ぶと警報が鳴る) ---
def alert_for_miscreant(people):
    for p in people:
        if p in ("Don", "John"):
            set_off_alarms()
            return p
    return ""
# --- After: 問い合わせ(副作用なし)とコマンド(副作用あり)に分ける ---
def find_miscreant(people):
    for p in people:
        if p in ("Don", "John"):
            return p
    return ""


def alert_for_miscreant(people):
    if find_miscreant(people):
        set_off_alarms()

デッドコードの削除

使われていないコードは「いつか使うかも」で残さず、バージョン管理に任せて消す。

# --- Before: 「いつか使うかも」でコメントアウトやフラグ分岐が残っている ---
def calculate_price(order):
    price = order.quantity * order.item_price
    # if order.is_member:  # 2023年に会員割引は廃止
    #     price *= 0.9
    return price

# def old_calculate_price(order):
#     ...  # 旧実装。念のため残しておく
# --- After: 消す。必要になったらバージョン管理から取り戻せる ---
def calculate_price(order):
    return order.quantity * order.item_price

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

  • YAGNI との関係:先回りの抽象化より進化的設計。リファクタリングが後からの設計変更のコストを下げる

  • 性能懸念:まず整えて、計測してから最適化する。きれいなコードの方がチューニングもしやすい

参考文献

  • Martin Fowler『リファクタリング(第2版)』オーム社

  • Kent Beck『Tidy First?』オライリー・ジャパン

  • Michael Feathers『レガシーコード改善ガイド』翔泳社