値オブジェクトと composed_of の使い方
背景
今回 rails の「値オブジェクト」について勉強する機会があったので、自分なりにわかりやすくまとめてみました。
これから値オブジェクトを使用しようと検討している方の参考になれば幸いです。
エンティティと値オブジェクト
まず、よく比較されるエンティティと値オブジェクトの違いは何かを説明いたします。
エンティティ
属性(カラム)の値に関わらず一意に識別することができるオブジェクト。
値オブジェクト
値が同じであれば、同じものとして扱うことができるため、値により一意に判別することができないオブジェクト。
アプリケーション上は、ある値に関連する処理やその値を値オブジェクトとして定義することで、うまくロジックを記述することができます。
次は、上記で説明した値オブジェクトの導入タイミングと使い方について説明していきます。
導入タイミング
先ほど紹介した値オブジェクトの導入タイミングとしては、同じようなロジックを複数のクラスに記述するときや、すでに同じようなロジックの処理がすでに記述され、ある1つの箇所にロジックの記述を1つにまとめたいときです。
例えば、UserクラスとCompanyクラスに住所の郵便番号を表すpost_codeカラムがあるとします。
この時、post_codeカラムに入力された値が、半角英数字かどうか判定するロジックを持つメソッドを作成したいとなった時、Userクラス、Companyクラスの両方にそのロジックを持つメソッドを定義するのではなく、PostCodeという値オブジェクトを作成することで、Userクラス、Companyクラスの両方からそのロジックを分離することができます。
また、このようにすることで、Userクラス、Companyクラス以外のクラスで同様のロジックが必要となった時、PostCodeという値オブジェクトからそのロジックを持つメソッドを持ってくれば良いということになります。
rails で値オブジェクトを導入するには、composed_of を使用します。
次にその composed_of の使い方を紹介します。
composed_of とは?
composed_of は、値オブジェクトを用いる属性(カラム)名を指定するためのものです。
第一引数に、値オブジェクトを用いるカラムを指定します。
ここで指定したカラム名が値オブジェクトのクラス名となります。
使用例は以下のとおりです。
# app/models/user.rb
class User < ApplicationRecord
composed_of :post_code,
class_name: 'PostCode',
mapping: %w(post_code value)
def post_code_valid?
post_code.valid?
end
end
# app/models/company.rb
class Company < ApplicationRecord
composed_of :post_code,
class_name: 'PostCode',
mapping: %w(post_code value)
def post_code_valid?
post_code.valid?
end
end
# app/models/post_code.rb
class PostCode
attr_reader :value
def initialize(value)
@value = value.to_i
end
def valid?
@value.to_s.chars.any? { |digit| ('1'..'5').include?(digit) }
end
end
上記のコードでは、User,Companyの各モデルに PostCode という値オブジェクトを作成し、User,Companyの各モデルが持つ post_code カラムの値の中に数値 1~5 のいずれかの値が含まれるかどうか判定するメソッド valid? を定義しました。
PostCode という値オブジェクトを作成し、その中に共通のロジック valid? メソッドを定義することで、User,Companyの各モデルが使用することができる共通のメソッドとして定義することができています。
本当に実装できているか確認してみます。
[1] pry(main)> User.last.post_code
=> 1112222
[2] pry(main)> Company.last.post_code
=> 6667777
[3] pry(main)> User.last.post_code_valid?
=> true
[4] pry(main)> Company.last.post_code_valid?
=> false
うまく実装できているようです。
composed_of のオプション
次に、composed_of の各オプションの特徴をまとめます。
こちらの記事を参考に表にまとめてみました。
表だけではわかりにくいオプションもあるので、下記に使用例をもとに説明していきます。
mapping は、データベースのカラムと値オブジェクトの属性を関連付けるためのものです。
これがないと、データベースのカラムと値オブジェクトの属性を関連付けることができないため、適切にデータの操作をすることができません。
先ほどのコードで mapping がないときとあるときの挙動の差を確認してみます。
# mapping はないとき
class User < ApplicationRecord
composed_of :post_code,
class_name: 'PostCode'
・・・
end
pry(main)> User.create(post_code: PostCode.new(12345))
=> NoMethodError: undefined method `post_code' for #<PostCode:0x00007f8e1c964d10 @value=12345>
# mapping があるとき
class User < ApplicationRecord
composed_of :post_code,
class_name: 'PostCode',
mapping: %w(post_code value)
・・・
end
pry(main)> User.create(post_code: PostCode.new(12345))
=> #<User:0x00007f8e221e5f20
id: 2,
post_code: 12345,
created_at: Sat, 10 Aug 2024 00:05:33.172252000 UTC +00:00,
updated_at: Sat, 10 Aug 2024 00:05:33.172252000 UTC +00:00>
上記のコードで mapping がない状態でインスタンスを作成しようとすると、NoMethodError となり、インスタンスを作成することができませんでした。
一方、mapping を指定すると、Userモデルの post_codeカラムに値を入れ、インスタンスを作成することができています。
このように mapping オプションがないと、値オブジェクトを使用したモデルのインスタンスの作成や削除をすることができなくなってしまいます。
allow_nil は、属性が nil であることを許可するためのものです。
このオプションのデフォルトは false ですが、 true に設定することで、属性が nil でもその値を保存することができます。
先ほどのコードで allow_nil がある時とない時の挙動の差を確認してみます。
# allow_nil がないとき
class Company < ApplicationRecord
composed_of :post_code,
class_name: 'PostCode',
mapping: %w(post_code value)
・・・
end
pry(main)> Company.create(post_code: nil)
=> NoMethodError: undefined method `value' for nil:NilClass
# allow_nil があるとき
class Company < ApplicationRecord
composed_of :post_code,
class_name: 'PostCode',
mapping: %w(post_code value),
allow_nil: true
・・・
end
pry(main)> Company.create(post_code: nil)
=> #<Company:0x00007f8e1d52ca80
id: 4,
post_code: nil,
created_at: Sat, 10 Aug 2024 00:38:11.893311000 UTC +00:00,
updated_at: Sat, 10 Aug 2024 00:38:11.893311000 UTC +00:00>
上記のコードで allow_nil: true がないと、 post_code カラムに nil を指定し、インスタンスを作成しようとしても、NoMethodError となり、インスタンスを作成することができませんでした。
一方、allow_nil: true と記述することで、インスタンスを作成することができました。
このように、allow_nil: true を指定することで、インスタンスの作成やカラムの値の更新を行うことができます。
まとめ
今回、値オブジェクトと値オブジェクトを作成するための composed_of の使い方やそのオプションをまとめてみました。
まだ使用する機会がなかったので、今後、似たようなロジックがあった時、値オブジェクトにできるかどうかを念頭に置きながら、業務を行っていきたいです。
参考
Discussion