Ruby on Railsで開発する際、モデルの属性として string
型のメールアドレス、integer
型の金額、string
型の郵便番号などをそのまま使うことはよくあります。しかし、これら組み込み型(プリミティブ型)を直接扱うことには、いくつかの潜在的な問題があります。
- 意味の欠如:
email = "test@example.com"
というコードだけでは、この文字列が「メールアドレス」という特定の意味やルールを持つことが伝わりにくい。 - ロジックの散在: メールアドレス形式の検証、金額のフォーマット、郵便番号のハイフン処理といった、値に関するロジックがモデル、コントローラー、ヘルパーなど様々な場所に散らばりがちです。
- 振る舞いの欠如: 値自身に関連する操作(例: 金額同士の加算、メールアドレスのドメイン取得)を、その値を使う側で毎回実装する必要が出てきます。
これらの課題を解決し、コードの可読性、保守性、データ整合性を高めるための設計パターンが「Valueオブジェクト(値オブジェクト)」です。ドメイン駆動設計(DDD)でも重要な概念とされています。
この記事では、Valueオブジェクトの基本概念、導入するメリット、Rubyでの作り方、そしてRails (ActiveRecord) での実用的な使い方を解説します。
Valueオブジェクトとは?
Valueオブジェクトは、値そのものを表現し、識別子(ID)を持たないオブジェクトです。単なるデータではなく、「意味のある値」をオブジェクトとしてカプセル化します。
主な特徴は以下の通りです。
- 値によって定義される: オブジェクトが持つ属性の値によって同一性が決まります。
Money.new(1000, 'JPY')
と別のMoney.new(1000, 'JPY')
は等価です。 - 不変 (Immutable): 一度作成したら状態(値)は変更できません。変更が必要な場合は新しいオブジェクトを生成します。これにより予期せぬ副作用を防ぎます。
- 等価性比較: 値が同じであれば等価とみなされるよう、
==
,eql?
,hash
メソッドを適切に実装します。 - 自己検証 (Self-Validation): 生成時に自身の値が妥当か(例: メールアドレス形式として正しいか)を検証できます。
例えば、"user@example.com"
というStringではなく、EmailAddress.new("user@example.com")
という不変で自己検証済みのオブジェクトとして扱うことで、より安全で表現力豊かなコードになります。
なぜValueオブジェクトを使うのか?メリット解説
Valueオブジェクトを導入することで、多くのメリットが得られます。
- コードの可読性と表現力向上:
amount
がMoney
型であることで、「金額」というドメインの概念がコード上に明確に表現され、意図が伝わりやすくなります。 - 関心の分離・凝集度の向上: 値に関するルールや振る舞い(バリデーション、フォーマット、計算など)を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オブジェクトを適切に導入することで、より堅牢で、変更に強く、理解しやすいコードベースを構築することが可能です。ぜひ、あなたのプロジェクトでの活用を検討してみてください。