Qiita Conference 2025

Qiita史上最多!豪華12名のゲストが登壇

特別講演ゲスト(敬称略)

ymrl、成瀬允宣、鹿野壮、伊藤淳一、uhyo、徳丸浩、ミノ駆動、みのるん、桜庭洋之、tenntenn、けんちょん、こにふぁー

3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

リファクタリングを推進しよう! 〜既存コードを安全に改善する技術〜

Last updated at Posted at 2025-03-21

みなさんは、日々の業務の中で「リファクタリング」に積極的に取り組んでいますか?

「リファクタリング」は、『リファクタリング 既存のコードを安全に改善する(第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によるコードレビューや自動生成が効果的に機能する
  • 保守性が向上し、新規開発時の工数やバグの発生率が低下する

まとめ

リファクタリングの推進は「リスクを減らし、コードの健康状態を維持し、未来の変化に対応できる仕組み」を作るための重要な戦略です。
テストを充実させることはアプリケーションコードに影響を与えませんし、十分なテストコードがある中でリファクタリングを行うことは、新しい機能を作るよりよっぽど簡単です。
次に自分を含むチームの誰かがこのコードを見たときを思ってぜひ一歩踏み出してみましょう!

3
1
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

Qiita Conference 2025 will be held!: 4/23(wed) - 4/25(Fri)

Qiita Conference is the largest tech conference in Qiita!

Keynote Speaker

ymrl、Masanobu Naruse, Takeshi Kano, Junichi Ito, uhyo, Hiroshi Tokumaru, MinoDriven, Minorun, Hiroyuki Sakuraba, tenntenn, drken, konifar

View event details
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?