0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

オブジェクト指向入門!Ruby on Rails の場合分けロジックを整理する!

0
Last updated at Posted at 2025-08-17

場合わけロジックを整理する

概要

この記事は、現場で役立つシステム設計の原則〜変更を楽で安全にするオブジェクト指向の実践技法を読み学習した内容を Ruby on Rails のコードに例を変換してまとめ直したものです。

また、書籍では Java のコードを例にしており、Ruby on Rails の慣習とは一部異なる部分があるので、その部分はオミットした内容となっています。

ソフトウェアを複雑にし、変更を大変にする原因の一つに、場合わけロジックがある。

例えば業務アプリケーションには顧客区分料金種別といった区分・分類が存在するが、それらには区分ごとに計算や特別な処理ロジックが存在する。

これらの区分ごとのロジックを書き分ける基本的な手段に if 文/switch 文がある。

しかし、複雑な区分体系を組み合わせる業務ロジックの場合、if 文/switch 文の条件分岐はどんどん肥大化し、見通しが悪く変更が大変となる。

この記事では、このような場合わけロジックを整理する設計方法を紹介する。

場合わけの条件やその後の処理をメソッドに抽出する

場合わけロジックを整理する方法として、まずは基本的な「メソッドの抽出」がある。

if customer_type == "child"
  fee = BASE_FEE * 0.5
end

例えば、上記のようなコードの判断条件とその後の処理をメソッドに抽出すると以下のようになる。

if is_child?(customer_type)
  fee = child_fee
end

def is_child?(customer_type)
  customer_type == "child"
end

def child_fee(base_fee)
  BASE_FEE * 0.5
end

このように、リファクタリングすると

  • 元のコードはメソッドを帯び出すだけのシンプルなコードに変わる
  • メソッド名を読むだけで「子供だったら子供料金にする」という意図がわかりやすくなる
  • 子供判定や子供料金の計算は、それぞれのメソッドへ変更箇所と影響範囲を閉じ込められる

else 句を使用せず早期リターンを使用する

もう一つの場合わけロジックの整理方法としては、else 句を使用しないこと。

def fee(customer_type)
  if is_child?(customer_type)
    result = child_fee
  elsif is_senior?(customer_type)
    result = senior_fee
  else
    result = adult_fee
  end

  result

例えば、上記のコードはローカル変数 result は不要であり、 return で直ちに結果を返却できる。(早期リターン)

さらに else 句を使用せずに早期リターンする書き方(ガード節)にすることも可能。

def fee(customer_type)
  if is_child?(customer_type)
    return child_fee
  elsif is_senior?(customer_type)
    return senior_fee
  else
    return adult_fee
  end
end

# ガード節を使用したコード
def fee(customer_type)
  return child_fee if is_child?(customer_type)
  return senior_fee if is_senior?(customer_type)

  adult_fee
end

このように、 else 句を見つけたら早期リターンやガード節を使用することで、場合わけロジックのコードをスッキリさせ可読性が向上しバグの混入を防ぎやすくなる。

早期リターンやガード節を使用しやすくするために前項の「メソッドの抽出」を行なっておくことも重要。

区分ごとのロジックを別クラスに分ける

大人、子供、シニアといった区分ごとのロジックをさらに独立させるためには、別のクラスに分けるやり方がある。

以下のようにクラス分けすることで、料金計算のみでなく名前ラベルなど他のロジックの置き場所として利用できる。

class AdultFee
  FEE = 100
  LABEL = "大人"

  def fee
    Yen.new(FEE)
  end

  def label
    LABEL
  end
end

class ChildFee
  FEE = 50
  LABEL = "子供"

  def fee
    Yen.new(FEE)
  end

  def label
    LABEL
  end
end

インターフェース宣言で、区分ごとのクラスを同じ「型」として扱う

区分ごとにクラスを分けると、ロジックの整理がしやすくなるが、区分クラスを使う側が AdultFee, ChildFee をいつも意識して使い分けなければならない。

それらのクラスの使い分けをプログラムの各所で if 文を用いて記述すると、場合わけロジックを整理するために作成したクラスの目的が失われてしまう。

そこで、区分ごとのクラスを同じ「型」として扱うことで、使い分けを意識せずに利用できるようにする。

Java では interface を使用することで、同じ「型」として扱うことができるが、Ruby ではモジュールを使用することで同様に同じ「型」を表現できる。

# 「型」として扱うためのモジュールを定義
module Fee
    # include先クラスのオブジェクトがyenメソッドを定義せず呼び出すとエラーをraiseする
  def yen
    raise NotImplementedError
  end

  def label
    raise NotImplementedError
  end
end

# モジュールをincludeしたクラス

class AdultFee
  include Fee # Feeモジュールをincludし、Fee型として扱う

  FEE = 100
  LABEL = "大人"

  def yen
    Yen.new(FEE)
  end

  def label
    LABEL
  end
end

class ChildFee
  include Fee

  FEE = 50
  LABEL = "子供"

  def yen
    Yen.new(FEE)
  end

  def label
    LABEL
  end
end
Fee型を扱うChargeクラス
class Charge
  def initialize(fee)
    @fee = validate(fee) # Fee型のオブジェクト全てを扱える
  end

  def yen
    @fee.yen
  end

  private

  def validate(fee)
    # is_a?でモジュールをincludeしたクラスであるかをチェックできる
    raise TypeError, "Fee module required" unless fee.is_a?(Fee)

    fee
  end
end

この仕組みを利用すると、例えば以下のような「予約」クラスでは、大人、子供、シニアの場合わけの if 文を書かずに、それらを意識せずに Fee 型のオブジェクトを add メソッドで追加するだけで、料金の合計を計算できる。

また、SeniorFee クラスを追加した場合でも、追加したクラス内で Fee モジュールを include すれば、Reservation クラスは全く変更する必要がない。

class Reservation
  def initialize(fees: [])
    @fees = fees
  end

  # 区分ごとのクラスを意識せずにFee型のオブジェクトを追加できる
  def add(fee)
    raise TypeError, "Fee module required" unless fee.is_a?(Fee)

    result = Array.new(@fees)
    result << fee

    Reservation.new(fees: result)
  end

  def total
    total = Yen.new(0)
    @fees.each { |fee| total = total.add(fee.yen) } # Yenクラスのaddは新しいYenオブジェクトを返す

    total
  end
end

このようにインターフェース宣言( Fee モジュール)と、区分ごとの専用クラス( AdultFee, ChildFee )を組み合わせて、区分ごとに異なるクラスのオブジェクトを「同じ型」として扱う仕組みを「多態」という。

上記の「予約」クラスのように、使う側のクラスが実際にどのような区分があるのか「知らない」ことが重要。

外部クラスについて「知らない」ことが多いほど、クラス間の結びつきが弱くなり、あるクラスの変更が他のクラスに影響せず独立性が高まる。

区分ごとのクラスのインスタンスを生成する

class FeeFactory
  def self.fee_by_name(name)
    if name == "adult"
      AdultFee.new
    elsif name == "child"
      ChildFee.new
    end
  end
end

区分ごとのクラスのインスタンスを生成する際に上記のような if 文による場合分けを排除するには、以下のようなマッピングを利用する。

class FeeFactory
  TYPES = {
    "adult" => AdultFee.new,
    "child" => ChildFee.new,
  }.freeze

  def self.fee_by_name(name)
    TYPES[name]
  end
end

# マッピングを利用したFeeFactoryクラスの利用例
FeeFactory.fee_by_name("adult")

区分オブジェクトで区分クラスの一覧を管理する

多態は区分ごとのロジックを整理する便利な仕組みだが、どういう区分体系であるかという区分ごとのクラスの一覧を管理するのには向いていない。

Java では enum クラス(列挙型)を使用することで、区分ごとのクラスの一覧を管理することができるが、Ruby では以下のようなクラスを作成することで、区分ごとのクラスを定数として一覧管理することができる。

このように区分クラスを単なる定数ではなく、「振る舞い」を持つオブジェクトとして管理するクラスを区分オブジェクトという。

class FeeType
  # 定数として定義し、一覧を管理する
  # FeeTypeクラスのインスタンスは以下の3つのみ存在する
  # 各インスタンスは内部でFeeの具象クラス(AdultFee等)のインスタンスを保持
  ADULT = new(AdultFee.new)
  CHILD = new(ChildFee.new)
  SENIOR = new(SeniorFee.new)

  attr_reader :fee

  def initialize(fee)
    @fee = fee # feeはFeeの具象クラス(AdultFee等)のインスタンス
  end
  
  def yen
    @fee.yen
  end
  
  def label
    @fee.label
  end

  def self.value_of(name)
    name_str = name.to_s.upcase
    
    # 定数名で直接アクセスを試行
    begin
      const_get(name_str)
    rescue NameError
      raise ArgumentError, "No enum constant #{self}::#{name_str}"
    end
  end
  
  # 外部からnewメソッドを呼び出せないようにする
  # これにより定数管理しているインスタンス以外を生成できなくする
  private_class_method :new
end
# 以下のように区分名から料金を計算できる
def fee_for(fee_type_name)
  fee_type = FeeType.value_of(fee_type_name)
  fee_type.yen
end

# 使用例
puts fee_for('adult')   # => 1000
puts fee_for('CHILD')   # => 500
puts fee_for(:adult)    # => 1000

状態の遷移ルールをわかりやすく記述する

業務アプリケーションでは、状態の遷移を管理することも重要な関心ごとの一つ。

ある状態が遷移できる次の状態には制限があるが、区分オブジェクトを利用することで、if, switch 文を使用せずに状態の遷移ルールをわかりやすく記述できる。

具体的には以下のような手順を踏む。

  • ある状態から遷移できる次の状態を Set で宣言する
  • 遷移元の状態をキー、遷移可能な状態の Set を値としたマッピングを宣言する
状態を表す区分オブジェクト
class State
  attr_reader :name

  def initialize(name)
    @name = name
    freeze
  end

  UNDER_REVIEW = new("審査中")
  APPROVED = new("承認済")
  IN_PROGRESS = new("実施中")
  COMPLETED = new("終了")
  SENT_BACK = new("差し戻し")
  SUSPENDED = new("中断中")
  VALUES = [UNDER_REVIEW, APPROVED, IN_PROGRESS, COMPLETED, SENT_BACK, SUSPENDED].freeze

  def self.values
    VALUES
  end

  def self.value_of(name)
    values.find { |value| value.name == name }
  end

  private_class_method :new
end
状態の遷移ルールを表すクラス
class StateTransitions
  # 遷移元の状態をキー、遷移可能な状態の Set を値としたマッピングを宣言する
  ALLOWED = {
    State::UNDER_REVIEW => Set[State::APPROVED, State::SENT_BACK],
    State::SENT_BACK => Set[State::UNDER_REVIEW, State::COMPLETED],
    State::APPROVED => Set[State::IN_PROGRESS, State::COMPLETED],
    State::IN_PROGRESS => Set[State::SUSPENDED, State::COMPLETED],
    State::SUSPENDED => Set[State::IN_PROGRESS, State::COMPLETED],
    State::COMPLETED => Set[],
  }.freeze


  # 遷移元の状態から遷移可能な状態かをチェックする
  def self.can_transition?(from, to)
    ALLOWED[from].include?(to)
  end
end
クラスの使用例
def can_transition_to?(new_state_name)
  new_state = State.value_of(new_state_name)
  StateTransitions.can_transition?(current_state, new_state)
end

参考文献

この記事は以下の情報を参考にして執筆しました。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?