Value オブジェクトと composed_of の使い方
Value(値)オブジェクト とは?
属性を表す住所や血液型などの値が同じでも同一性を判断できない情報のことをいいます。
例えば、同じ金額の商品がAとBで2つあり、AとBの商品は同じ商品であるということの証明ではないということ。
Valueオブジェクトの主な役割は、住所や名前といった値をひとつのクラスに切り出して、影響範囲を最小限にすることです。
rails では Valueオブジェクト を作成するために、composed_of というメソッドが用いられます。
Valueオブジェクトに切り出す目安は、複数のクラスから参照されるようになったときです。
その時にはじめて導入することで、モデルクラスの肥大化を抑えることができます。
(リファクタリングの時とかに使用できそう。)
エンティティ とは?
属性の値に関わらず一意に識別されるオブジェクトのこと。
例えば、性、名、生年月日が同じ2つのユーザーがいてもデータベース上では、異なるオブジェクトであるということ。
また、エンティティは同一性を識別するための情報を表す識別子というものを持つ。
composed_of とは?
複数の属性をひとつのオブジェクトとしてまとめるための仕組みです。
composed_of メソッドの使用例を説明します。
まず、Userモデルが下記のようなコードで定義されていたとします。また、Userモデルにはnameとemailというカラムが定義されているとします。
class User < ApplicationRecord
def name_and_email
return "私の名前は、#{name}です。アドレスは、#{email}です。"
end
def specific_string_check
name.include?("ブチャ")
end
end
そこでItemモデルにもnameというカラムがあり、specific_string_check メソッドを使用し、特定の文字列があるか確認しようとしてみました。
しかし、specific_string_check メソッドは、Userクラスのインスタンスメソッドのため、Item.last.specific_string_check とすると、NoMethodError: undefined method `specific_string_check' というエラーになります。
そこで、下記のようにItemクラスにspecific_string_check
メソッドを定義してみました。
# app/models/item.rb
class Item < ApplicationRecord
def specific_string_check
name.include?("ブチャ")
end
end
pry(main)> item = Item.last
pry(main)> item.specific_string_check
=> false
確かにこれでもspecific_string_check
メソッドを実行することができますが、そしたら他のクラスでもnameカラムがあり、特定の文字列を調べたいとき、specific_string_check
メソッドを定義しなおさなければいけません。
そうすると、各モデルにspecific_string_check
メソッドを定義し直さなければならず、各モデルが肥大化してしまいます。
このようなときは、Valueオブジェクトを作成するためのcomposed_ofメソッドを使用することで、モデルの肥大化を抑えることができます。
使用例は以下の通りです。
# app/models/user.rb
class User < ApplicationRecord
composed_of :profile,
mapping: [%w[name name], %w[email email]],
converter: ->(name, email) { Profile.new(name, email) }
end
# app/models/item.rb
class Item < ApplicationRecord
composed_of :profile,
mapping: [%w[name name], %w[email email]],
converter: ->(name, email) { Profile.new(name, email) }
end
# app/models/profile.rb
class Profile
attr_accessor :name, :email
def initialize(name, email)
@name, @email = name, email
end
# ユーザーの名前とアドレスを返す
def name_and_email
return "私の名前は、#{name}です。アドレスは、#{email}です。"
end
# 名前に特定の文字列を含むかどうかの判定
def specific_string_check
name.include?("ブチャ")
end
end
[1] pry(main)> user = User.last
(0.6ms) SELECT sqlite_version(*)
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]]
=> #<User id: 140, provider: "email", uid: "buronobutyarathi@gmail.com", allow_password_change: [FILTERED], name: "ブチャラティ", nickname: nil, image: nil, email: "buronobutyarathi@gmail.com", created_at: "2023-06-17 21:03:34.236779000 +0000", updated_at: "2023-06-18 01:28:03.412723000 +0000">
[2] pry(main)> user_profile = user.profile
=> #<Profile:0x00007fafe8216750 @email="buronobutyarathi@gmail.com", @name="ブチャラティ">
[3] pry(main)> user_profile.name_and_email
=> "私の名前は、ブチャラティです。アドレスは、buronobutyarathi@gmail.comです。"
[4] pry(main)> user_profile.specific_string_check
=> true
[5]pry(main)> item = Item.last
=> #<Item:0x00007fcd4f6d7e68 id: 6, name: "シャンプーF", ...
[6]pry(main)> item_profile = item.profile
=> #<Profile:0x00007fcd5032d870 @email=nil, @name="シャンプーF">
[3] pry(main)> item_profile.specific_string_check
=> false
上記のコードは、UserモデルとItemモデルにおいて、共通のプロフィール情報をProfileオブジェクトとして組み込むための設定を行っています。
specific_string_check
メソッドは、Userのnameに特定の文字列("ブチャ")が含まれているかどうかを判定します。
composed_ofを使用し、ProfileクラスをValueオブジェクトとして切り出すことで、あるメソッドを複数のクラスから参照することができ、コードが煩雑になることを防ぐことができます。
composed_of のオプション
最後に、composed_of のオプションの特徴をまとめます。こちらの記事を参考にまとめてみました。
:class_name
Valueオブジェクトのクラス名を指定することができます。 Value オブジェクト生成時にこのクラス名が使用されます。
:mapping
モデル属性(今回だとUserやItemモデルのこと)と値オブジェクト(今回でいうところのProfileクラス)の属性とのマッピングを指定します。
各マッピングは配列で表され、最初の項目はモデル属性の名前、2 番目の項目は Value オブジェクトの属性名です。
マッピングを定義する順序によって、属性がValue オブジェクトのコンストラクタに送信される順序が決まります。
:allow_nil
組み込まれるオブジェクトがnil
(存在しない)であることを許可するかどうかを指定するもの です。(nilを許可するかどうかをtrue と false で判断する。)
通常、composed_of
メソッドで組み込まれるオブジェクトは、必須の関連として扱われます。つまり、モデルの属性に対応するオブジェクトが存在しない場合にはエラーが発生します。
しかし、allow_nil
オプションを使用することで、組み込まれるオブジェクトがnil
であることを許可することができます。これにより、関連するオブジェクトが存在しない場合や、オブジェクトの属性が空である場合にエラーを回避することができます。
:constructor
Value オブジェクトを初期化するために呼び出されるコンストラクタ・メソッドまたは Proc の名前を指定するシンボルです。
コンストラクタは :mapping オプションで定義された順序で、マッピングされたすべての属性を引数として渡され、それらを使用して :class_name オブジェクトをインスタンス化します。デフォルトは :new です。
:converter
新しい値が値オブジェクトに代入されるときに呼び出される :class_name のクラスメソッドまたは Proc の名前を指定するシンボルです。
:converterには、代入に使用される単一の値が渡され、新しい値が :class_name のインスタンスでない場合にのみ呼び出されます。
:allow_nil を true に設定すると、:converter は nil を返して代入をスキップすることができます。
以上がValue オブジェクトと composed_of の説明になります。
参考
Discussion