みなさんは、日々の業務の中で「リファクタリング」に積極的に取り組んでいますか?
「リファクタリング」は、『リファクタリング 既存のコードを安全に改善する(第2版)』(マーティン・ファウラー著)でも語られている通り、既存のコードの振る舞いを変えずに内部構造を改善する作業です。ですが、多くのエンジニアが実践できずにいます。
その最大の理由は「リスク」への不安です。
リファクタリングを阻むリスク
「挙動が変わってしまったらどうしよう」「既存機能が壊れてしまったら責任を負えるだろうか」と、多くの人がリファクタリングに躊躇します。しかし、この問題への対策はすでに知られています。それが「テストの充実」です。
なぜテストがリファクタリングに必須か
コードの振る舞いが変わっていないことを保証する唯一の手段は、十分なテストしかありません。
-
テストがないコードは、触ること自体がリスクになる
-
テストがあれば、新人や新規プロジェクト参画者でも安全にコードを修正できる
-
振る舞いが明確に定義されているため、変更や改善の範囲を容易に特定できる
また、最近では AI技術を活用してテストコードを自動生成することも可能になっており、テストを書くハードルは下がっています。
テストコードはアプリの品質を向上させ、バグの発見を助けるものであり、よっぽど変な書き方をしていない限りアプリの動作を妨げることはないのでどんどん書いていきましょう!
リファクタリング実践例:誰でもできる最初の一歩
では、実際にどのようなリファクタリングをすれば良いのか、具体例を挙げてみましょう。
例:メソッドの抽出/クラスの抽出
次のようなコードを見てください。
# invoice.rb
class Invoice
attr_reader :items, :customer_type
def initialize(items, customer_type)
@items = items
@customer_type = customer_type
end
def total_price
total = 0
items.each do |item|
if customer_type == :regular
total += item[:price] * 0.9
elsif customer_type == :vip
total += item[:price] * 0.8
else
total += item[:price]
end
end
total + (total * 0.1) # 税金を追加
end
end
このコードには以下のような問題点があります。
- 割引計算が total_price メソッド内に直接書かれている
- ロジックが分離されておらず、将来的に拡張しづらい
- 計算の再利用が難しい(他の計算処理で使えない)
振る舞いを保証するためにテストコードを書いておきましょう。
# invoice_spec.rb
require 'rspec'
require_relative 'invoice'
RSpec.describe Invoice do
let(:items) { [{ price: 100 }, { price: 200 }, { price: 300 }] }
context "when customer is regular" do
it "calculates total price with 10% discount and 10% tax" do
invoice = Invoice.new(items, :regular)
# 計算式: (100 * 0.9 + 200 * 0.9 + 300 * 0.9) * 1.1 = 594.0
expect(invoice.total_price).to eq(594.0)
end
end
context "when customer is VIP" do
it "calculates total price with 20% discount and 10% tax" do
invoice = Invoice.new(items, :vip)
# 計算式: (100 * 0.8 + 200 * 0.8 + 300 * 0.8) * 1.1 = 528.0
expect(invoice.total_price).to eq(528.0)
end
end
context "when customer type is not specified" do
it "calculates total price without discount but with 10% tax" do
invoice = Invoice.new(items, :unknown)
# 計算式: (100 + 200 + 300) * 1.1 = 660.0
expect(invoice.total_price).to eq(660.0)
end
end
end
まずは計算ロジックを抜き出してロジックを分離していきます。
class Invoice
attr_reader :items, :customer_type
def initialize(items, customer_type)
@items = items
@customer_type = customer_type
end
def total_price
subtotal = items.sum { |item| apply_discount(item[:price]) }
subtotal + tax(subtotal)
end
private
def apply_discount(price)
case customer_type
when :regular then price * 0.9
when :vip then price * 0.8
else price
end
end
def tax(amount)
amount * 0.1
end
end
✅ apply_discount メソッドを抽出し、可読性が向上
✅ 計算ロジックの再利用が容易になった
次に、 ストラテジーパターンを活用して割引計算を DiscountStrategy クラスに分離 します。
class DiscountStrategy
def apply(price)
raise NotImplementedError
end
end
class RegularDiscount < DiscountStrategy
def apply(price)
price * 0.9
end
end
class VIPDiscount < DiscountStrategy
def apply(price)
price * 0.8
end
end
class NoDiscount < DiscountStrategy
def apply(price)
price
end
end
# コンテキスト
class Invoice
attr_reader :items, :discount_strategy
def initialize(items, discount_strategy)
@items = items
@discount_strategy = discount_strategy
end
def total_price
subtotal = items.sum { |item| discount_strategy.apply(item[:price]) }
subtotal + tax(subtotal)
end
private
def tax(amount)
amount * 0.1
end
end
# 使用例
strategy = case customer_type
when :regular then RegularDiscount.new
when :vip then VIPDiscount.new
else NoDiscount.new
end
invoice = Invoice.new(items, strategy)
invoice.total_price
✅ 割引計算の責務を DiscountStrategy クラスに分離し、Invoice クラスの役割を明確化
✅ 新しい割引ルールを追加する際の拡張性が向上
このように小さな一歩を積み重ねて、安全にリファクタリングを進めることが重要です。
未来の変更可能性を高める
リファクタリングを推進するもう一つの理由は、将来の変更を容易にするためです。開発を進めるとき、コードを書く時間よりも読む時間の方が圧倒的に占める割合は大きいですよね?そう、コードは書かれるよりも頻繁に読まれています。つまり、将来的な可読性の確保こそ、継続的開発のための重要な投資です。
また、コードが分かりやすいことは人間だけでなくAIにとってもメリットがあります。
- AIによるコードレビューや自動生成が効果的に機能する
- 保守性が向上し、新規開発時の工数やバグの発生率が低下する
まとめ
リファクタリングの推進は「リスクを減らし、コードの健康状態を維持し、未来の変化に対応できる仕組み」を作るための重要な戦略です。
テストを充実させることはアプリケーションコードに影響を与えませんし、十分なテストコードがある中でリファクタリングを行うことは、新しい機能を作るよりよっぽど簡単です。
次に自分を含むチームの誰かがこのコードを見たときを思ってぜひ一歩踏み出してみましょう!