リファクタリングとは¶
定義:外部から見た振る舞いを変えずに、内部構造を改善すること
目的:可読性・変更容易性を高め、バグを見つけやすくする
いつリファクタリングするか¶
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『レガシーコード改善ガイド』翔泳社