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

クラス内で繰り返し発生するまったく同じ条件分岐をStrategyパターンでリファクタリングする

1
Last updated at Posted at 2026-09-30

はじめに

case式を使ったまったく同じ条件分岐が同じクラス内に複数回登場するようなコードはStrategyパターンを使ってリファクタリングできる可能性があります。

この記事ではサンプルコードを使ってこのリファクタリング方法を説明します。

リファクタリング前のコード

たとえば以下のようなクラスがあったとします。
このクラスはitemの型(クラス)に応じてシリアライズ(ここではハッシュオブジェクトへの変換)の形式を切り替えています。

serialize_compactメソッドとserialize_fullメソッドで、まったく同じ条件分岐(case/in)が登場している点に注目してください。

class MediaItemSerializer
  def initialize(item)
    @item = item
  end

  def serialize_compact
    case item
    in Book
      {
        type: "book",
        title: item.title,
        creator: item.author,
        price_label: price_label(item.price),
      }
    in Album
      {
        type: "album",
        title: item.title,
        creator: item.artist,
        price_label: price_label(item.price),
      }
    in Movie
      {
        type: "movie",
        title: item.title,
        creator: item.director,
        price_label: price_label(item.price),
      }
    end
  end

  def serialize_full
    base = serialize_compact

    # serialize_compactと同じcase/inがここにも登場する
    case item
    in Book
      base.merge(isbn: item.isbn, pages: item.pages)
    in Album
      base.merge(
        track_count: item.tracks.size,
        duration_label: duration_label(item.duration),
      )
    in Movie
      base.merge(
        duration_label: duration_label(item.duration),
        rating: item.rating,
      )
    end
  end

  private

  attr_reader :item

  def price_label(price)
    "¥#{price}"
  end

  def duration_label(seconds)
    "#{seconds / 60}分"
  end
end

ちなみに上のコードの実行結果はこんなイメージです。
(MediaItemSerializerに渡したオブジェクトに応じて、出力結果の形式が切り替わる)

MediaItemSerializer.new(book).serialize_compact
#=> { type: "book", title: "Ruby入門", creator: "山田太郎", price_label: "¥1200" }

MediaItemSerializer.new(album).serialize_full
#=> { type: "album", title: "BEST HITS", creator: "スズキバンド", price_label: "¥2500",
#     track_count: 12, duration_label: "74分" }

# NOTE: 実際の呼び出しでは以下のようなループ処理で呼ばれる想定
current_library.media_items.map do |item|
  # itemはBook/Album/Movieのいずれか
  MediaItemSerializer.new(item).serialize_compact
end

上のコードの問題点

先ほどのコードには以下のような問題点があります。

  • 将来もし、itemとしてBook/Album/Movie以外の別のデータ型が渡されるようになったら、serialize_compactとserialize_fullに同じ条件分岐(たとえばin Magazine)を追加しないといけない
  • Book用、Album用、Movie用の処理がクラス内で分散してしまう。BookならBook用のロジックを、AlbumならAlbum用のロジックを、MovieならMovie用のロジックをまとめて見たいケースもあるはず
  • 将来もし、serialize_minimalのように、よく似たメソッドを追加することがあれば、まったく同じ条件分岐がさらに増殖する

上に挙げたサンプルコードは、説明用に短くしてありますが、実務であれば、

  • case/inの条件分岐(形式を切り替えたいデータ型)がもっとたくさんある
  • データごとに処理を切り替えたいメソッドがもっとたくさんある

といった状況が十分考えられます。

その場合はここで見たコードよりも、さらに保守性が低く、コードの見通しも悪いプログラムができあがります。

Strategyパターンでリファクタリングしてみる

そこで、先ほどのコードをStrategyパターンを使ってリファクタリングしてみることにします。

Strategy パターン(ストラテジーパターン)は、アルゴリズムを共通のインターフェースを持つ独立したクラスとして定義し、実行時に交換可能にすることで、処理内容の変更を利用側のコードから独立させるデザインパターンである。これにより、複数のアルゴリズムを状況に応じて切り替えながら、利用側の構造を変更せずに拡張や保守を行えるようになる。

Strategy パターン - Wikipedia

今回は既存のMediaItemSerializerに加えて、以下のクラス図のように、

  • BaseStrategy
    • BookStrategy
    • AlbumStrategy
    • MovieStrategy

という4つのクラスを追加してシリアライズ処理を実装します。

少し長いですが、リファクタリング後のコードは以下のようになります。

class MediaItemSerializer
  def initialize(item)
    @strategy =
      case item
      in Book then BookStrategy.new(item)
      in Album then AlbumStrategy.new(item)
      in Movie then MovieStrategy.new(item)
      end
  end

  def serialize_compact
    @strategy.serialize_compact
  end

  def serialize_full
    @strategy.serialize_full
  end

  class BaseStrategy
    def initialize(item)
      @item = item
    end

    def serialize_compact
      compact_attributes
    end

    def serialize_full
      compact_attributes.merge(extra_attributes)
    end

    private

    attr_reader :item

    # サブクラスで実装漏れがあったら例外を発生させる
    def compact_attributes
      raise NotImplementedError
    end

    # 同上
    def extra_attributes
      raise NotImplementedError
    end

    def price_label(price)
      "¥#{price}"
    end

    def duration_label(seconds)
      "#{seconds / 60}分"
    end
  end

  class BookStrategy < BaseStrategy
    private

    def compact_attributes
      {
        type: "book",
        title: item.title,
        creator: item.author,
        price_label: price_label(item.price),
      }
    end

    def extra_attributes
      { isbn: item.isbn, pages: item.pages }
    end
  end

  class AlbumStrategy < BaseStrategy
    private

    def compact_attributes
      {
        type: "album",
        title: item.title,
        creator: item.artist,
        price_label: price_label(item.price),
      }
    end

    def extra_attributes
      {
        track_count: item.tracks.size,
        duration_label: duration_label(item.duration),
      }
    end
  end

  class MovieStrategy < BaseStrategy
    private

    def compact_attributes
      {
        type: "movie",
        title: item.title,
        creator: item.director,
        price_label: price_label(item.price),
      }
    end

    def extra_attributes
      {
        duration_label: duration_label(item.duration),
        rating: item.rating,
      }
    end
  end
end

このリファクタリングの注目ポイント

このリファクタリングの注目ポイントを以下にまとめます。

条件分岐が1箇所になった

データ型に応じて条件分岐しているのはMediaItemSerializer#initializeの中だけです。

class MediaItemSerializer
  def initialize(item)
    @strategy =
      case item
      in Book then BookStrategy.new(item)
      in Album then AlbumStrategy.new(item)
      in Movie then MovieStrategy.new(item)
      end
  end

  # ...

将来データ型が増えても、条件分岐の修正が必要になるのはこの1箇所だけで済みます。

データ型ごとに処理を担当するクラスが分かれた

Bookに対応するStrategyはBookStrategy、Albumに対応するStrategyはAlbumStrategy、というように、各データ型に対応するStrategyクラスを定義しました。

そのため、Book用のシリアライズ処理を確認したいと思ったら、BookStrategyだけに着目すればよくなります。

# Book用のシリアライズ処理を確認したいなら、このクラスだけを見ればよい
class BookStrategy < BaseStrategy
  private

  def compact_attributes
    {
      type: "book",
      title: item.title,
      creator: item.author,
      price_label: price_label(item.price),
    }
  end

  def extra_attributes
    { isbn: item.isbn, pages: item.pages }
  end
end

共通の処理はBaseStrategyに定義した

BookでもAlbumでもMovieでも使う、共通の処理はBaseStrategyクラスに定義しています。

class BaseStrategy
  # ...

  def serialize_compact
    compact_attributes
  end

  def serialize_full
    compact_attributes.merge(extra_attributes)
  end

  private

  # ...

  def price_label(price)
    "¥#{price}"
  end

  def duration_label(seconds)
    "#{seconds / 60}分"
  end
end

BookStrategyやAlbumStrategyはBaseStrategyを継承しているので、共通処理がそのまま使えます。

class BookStrategy < BaseStrategy
  # ...
end

class AlbumStrategy < BaseStrategy
  # ...
end

class MovieStrategy < BaseStrategy
  # ...
end

呼び出し側のコードは同じ

MediaItemSerializerのAPIは変わっていないため、呼び出し側のコードは修正不要です。
また、メソッドの戻り値も修正前と同じです。

MediaItemSerializer.new(book).serialize_compact
#=> { type: "book", title: "Ruby入門", creator: "山田太郎", price_label: "¥1200" }

MediaItemSerializer.new(album).serialize_full
#=> { type: "album", title: "BEST HITS", creator: "スズキバンド", price_label: "¥2500",
#     track_count: 12, duration_label: "74分" }

新しいメソッドが増えても条件分岐は増えない

将来もし、serialize_minimalのような、よく似たメソッドが増えたとしても、条件分岐は増殖しません。
BaseStrategyとBookStrategy/AlbumStrategy/MovieStrategyに必要な処理を書き足すだけで済みます。

 class BaseStrategy
   # ...

+  def serialize_minimal
+    minimal_attributes
+  end

   private

   # ...
   
+  # サブクラスで実装漏れがあったら例外を発生させる
+  def minimal_attributes
+    raise NotImplementedError
+  end

   # ...
 class BookStrategy < BaseStrategy
   private

   # ...
   
+  def minimal_attributes
+    { type: "book", title: item.title, creator: item.author }
+  end
 end

(AlbumStrategy / MovieStrategy にも同様に minimal_attributes を追加)

まとめ

というわけで、この記事ではクラス内で繰り返し発生する同じ条件分岐をStrategyパターンでリファクタリングする方法を紹介しました。

「似て非なるものをまとめて処理したい」と思うようなメソッドが複数個あると、冒頭で挙げたサンプルコードのようなプログラムが作られがちです。
「まったく同じ条件分岐をあっちでもこっちでも書いてるわ〜」と思ったら、この記事で紹介したリファクタリング方法を検討してみてください!

あわせて読みたい

Strategyパターンは、いわゆる「デザインパターン」の一種です。

RailsのようなWebアプリケーションでも応用できるパターンもいくつかあるので、「デザインパターン」をしっかり勉強したことがない人は、以下のような書籍もチェックしてみましょう。

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