【Rails】Valueオブジェクト入門|値に意味と振る舞いを持たせる

Ruby on Railsで開発する際、モデルの属性として string 型のメールアドレス、integer 型の金額、string 型の郵便番号などをそのまま使うことはよくあります。しかし、これら組み込み型(プリミティブ型)を直接扱うことには、いくつかの潜在的な問題があります。

  • 意味の欠如: email = "test@example.com" というコードだけでは、この文字列が「メールアドレス」という特定の意味やルールを持つことが伝わりにくい。
  • ロジックの散在: メールアドレス形式の検証、金額のフォーマット、郵便番号のハイフン処理といった、値に関するロジックがモデル、コントローラー、ヘルパーなど様々な場所に散らばりがちです。
  • 振る舞いの欠如: 値自身に関連する操作(例: 金額同士の加算、メールアドレスのドメイン取得)を、その値を使う側で毎回実装する必要が出てきます。

これらの課題を解決し、コードの可読性、保守性、データ整合性を高めるための設計パターンが「Valueオブジェクト(値オブジェクト)」です。ドメイン駆動設計(DDD)でも重要な概念とされています。

この記事では、Valueオブジェクトの基本概念、導入するメリット、Rubyでの作り方、そしてRails (ActiveRecord) での実用的な使い方を解説します。

目次

Valueオブジェクトとは?

Valueオブジェクトは、値そのものを表現し、識別子(ID)を持たないオブジェクトです。単なるデータではなく、「意味のある値」をオブジェクトとしてカプセル化します。

主な特徴は以下の通りです。

  1. 値によって定義される: オブジェクトが持つ属性の値によって同一性が決まります。Money.new(1000, 'JPY') と別の Money.new(1000, 'JPY') は等価です。
  2. 不変 (Immutable): 一度作成したら状態(値)は変更できません。変更が必要な場合は新しいオブジェクトを生成します。これにより予期せぬ副作用を防ぎます。
  3. 等価性比較: 値が同じであれば等価とみなされるよう、==, eql?, hash メソッドを適切に実装します。
  4. 自己検証 (Self-Validation): 生成時に自身の値が妥当か(例: メールアドレス形式として正しいか)を検証できます。

例えば、"user@example.com" というStringではなく、EmailAddress.new("user@example.com") という不変で自己検証済みのオブジェクトとして扱うことで、より安全で表現力豊かなコードになります。

なぜValueオブジェクトを使うのか?メリット解説

Valueオブジェクトを導入することで、多くのメリットが得られます。

  • コードの可読性と表現力向上: amountMoney 型であることで、「金額」というドメインの概念がコード上に明確に表現され、意図が伝わりやすくなります。
  • 関心の分離・凝集度の向上: 値に関するルールや振る舞い(バリデーション、フォーマット、計算など)をValueオブジェクト内に集約できます。モデルや他のクラスはこれらの詳細を知る必要がなくなり、スリムになります。
  • データ整合性と安全性向上: 不変性と自己検証により、不正な値の生成や意図しない変更を防ぎ、システム全体の値の一貫性を保ちやすくなります。
  • 再利用性の向上: Address オブジェクトなど、同じ種類の値を扱う共通のロジックを再利用できます。
  • エラーの早期発見: 不正な値でオブジェクトを作ろうとした時点でエラーを発生させ、問題の発見を早めることができます。

RubyでのValueオブジェクトの作り方

いくつかの方法がありますが、基本的な考え方は共通しています。

1. 基本的なクラス実装

通常のクラスで定義し、不変性と等価性を意識して実装します。

class EmailAddress
  attr_reader :value

  def initialize(value)
    # 自己検証: 不正な値ならエラー
    raise ArgumentError, "Invalid email format" unless value.match?(URI::MailTo::EMAIL_REGEXP)
    # 値を正規化・不変に
    @value = value.downcase.freeze
    # オブジェクト自体も不変に
    freeze
  end

  # 値に関する振る舞いを定義
  def domain
    value.split('@').last
  end

  # 値に基づいた等価性比較の実装
  def ==(other)
    other.is_a?(EmailAddress) && other.value == value
  end
  alias_method :eql?, :==

  def hash
    value.hash
  end

  # 文字列として扱いたい場合のために
  def to_s
    value
  end
end

# 使用例
email1 = EmailAddress.new("User@example.com")
email2 = EmailAddress.new("user@example.com")
puts email1 == email2 #=> true
puts email1.domain  #=> "example.com"
# email1.value = "new@example.com" #=> FrozenError (変更不可)

==, eql?, hash の実装は、ハッシュのキーとして使ったり、配列のユニーク操作 (uniq) を正しく動作させたりするために重要です。

2. Structの利用

Struct は属性定義と基本的な等価性比較を簡潔に記述できますが、不変性は freeze を使って自分で保証する必要があります。

EmailAddressStruct = Struct.new(:value) do
  def initialize(value)
    raise ArgumentError, "Invalid email format" unless value.match?(URI::MailTo::EMAIL_REGEXP)
    super(value.downcase.freeze)
    freeze
  end
  # ... 振る舞いは別途定義 ...
end

3. gemの利用 (参考)

values gem や dry-struct gem などを使うと、上記のような実装をより簡単に記述できる場合もあります。

Rails (ActiveRecord) でValueオブジェクトを使う

モデルの属性としてValueオブジェクトを使うには、DBカラムとの型変換が必要です。Rails 5以降の Attributes API (Custom Types) が推奨されます。

1. カスタム型クラス作成: DBとの相互変換ロジックを実装します。

# app/types/email_address_type.rb
class EmailAddressType < ActiveRecord::Type::Value
  # DBから読み込み時 (DB String -> EmailAddress Object)
  def cast(value)
    # 不正な値の場合、nilを返すかエラーにするかは設計次第
    EmailAddress.new(value.to_s) rescue nil
  end

  # DBへ書き込み時 (EmailAddress Object -> DB String)
  def serialize(email_address)
    # EmailAddressオブジェクトでなければnilを返すなど
    email_address.to_s if email_address.is_a?(EmailAddress)
  end

  # Active Recordが型変更を検知するために必要 (Rails 5.2+)
  def changed_in_place?(raw_old_value, new_value)
    cast(raw_old_value) != new_value
  end
end

2. カスタム型登録: イニシャライザ (config/initializers/types.rb など) で登録します。

# config/initializers/types.rb
ActiveRecord::Type.register(:email_address, EmailAddressType)

3. モデルでの利用: attribute メソッドで属性と型を指定します。

# app/models/user.rb
class User < ApplicationRecord
  attribute :email, :email_address

  # モデルレベルのバリデーションも組み合わせることが多い
  validates :email, presence: true
end

これで、User モデルの email 属性は EmailAddress オブジェクトとして扱われ、user.email.domain のようなメソッド呼び出しが可能になります。DBには文字列として保存されます。

フォームでの扱い:
通常、form_with などで f.email_field :email のように記述すれば、Railsが適切に処理してくれます。コントローラーのStrong Parametersでも params.require(:user).permit(:email) のように通常通り扱え、Userオブジェクトへの代入時にカスタム型が機能してValueオブジェクトに変換されます(不正な入力は通常 nil となり、モデルのバリデーションで検知できます)。

その他の方法 (参考):
Railsには他に composed_of (古い) や serialize といった機能もありますが、柔軟性や型安全性の観点から、現在はCustom Typesが最も推奨される方法です。

Valueオブジェクト利用時の注意点

  • 導入範囲の検討: すべてをValueオブジェクトにする必要はありません。ドメインにおける重要な概念、複雑なルールを持つ値、再利用したい値などに限定して導入を検討しましょう。過剰な設計は避け、シンプルさも大切にします。
  • 不変性の徹底: 必ず不変オブジェクトとして実装します。予期せぬバグの温床となります。
  • パフォーマンス: 多くのオブジェクトが生成されるため、極端なパフォーマンス要件がある場合は影響を測定・考慮する必要がありますが、通常は問題になりません。
  • DBスキーマ: Custom Typesを使う場合、DBスキーマはプリミティブ型(文字列、数値など)のままです。serialize を使う場合は、保存形式(YAML/JSON)やカラム型(text など)を検討します。

まとめ

Valueオブジェクトは、単なるデータを「意味のある値」としてオブジェクト化することで、Railsアプリケーションの品質を向上させる強力な設計パターンです。

  • コードの可読性を高め、ドメインの概念を明確に表現します。
  • 値に関するロジックを集約し、安全性再利用性を高めます。
  • 不変性により、システムの予測可能性を高めます。
  • Railsでは Custom Types (Attributes API) を使うことで、ActiveRecordとスムーズに統合できます。

プリミティブ型を直接扱う代わりにValueオブジェクトを適切に導入することで、より堅牢で、変更に強く、理解しやすいコードベースを構築することが可能です。ぜひ、あなたのプロジェクトでの活用を検討してみてください。

未経験からエンジニアへ転職!おすすめの転職サービスはこちら

「未経験だけどエンジニアになりたい…」「IT業界に興味があるけど、どこから始めるべきかわからない…」
そんな方におすすめなのが、プログラミングスクールを活用した転職活動です。
実績豊富なスクールを利用すれば、未経験からでもエンジニアとしての転職がぐっと近づきます!

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次