この記事はエイチーム引越し侍 / エイチームコネクトの社員による、Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2020 19日目の記事です。
はじめに
Railsでは、「Skinny Controller, Fat Model」という方針があります。
アプリケーションの主要なロジックをモデルに置き、
コントローラ(やビュー)はそのレイヤーでしかできないことに役割を限定するという方針です。
この方針に則ることで、各レイヤー間の依存関係をわかりやすくし、
影響範囲を明確にすることでできます。
しかし、ビジネスロジックをモデルに集中させるため、
適切にモデルを作成しないと、モデルが肥大化し過ぎてしまいます。
そのモデルの肥大化の対策の1つとして、値オブジェクトの導入があります。
値オブジェクトとは
値オブジェクトとは、ドメイン駆動設計ででてくる
ドメインモデルをコードで表現するためのパターンの1つです。
ドメインとは、アプリケーションが対象とする問題領域のことで、
そのドメインを分析して、構成概念を抽出することをモデリングといいます。
そして、モデリングの結果得られる概念のことをドメインモデルといいます。
ドメインモデルは、その概念に関する属性と振る舞いを持ちます。
このドメインモデルをオブジェクトとして表現するものとして、
「エンティティ」と先程述べた「値オブジェクト」があります。
エンティティと値オブジェクトの特徴
エンティティと値オブジェクトは、それぞれ下記のような特徴があります。
| エンティティ | 値オブジェクト | |
|---|---|---|
| 同一性 | 識別子が同じならば同一とみなす | 属性の値が全て同じなら同一とみなす |
| 可変性 | 生成後に属性を変化させることができる | 生成後に属性が変化することがない |
エンティティの例
- 社員
- たとえ同じ名前の社員が2人いても、その2人は別人
- 誕生日を迎えて、年齢という属性が変化しても、別の人間にはならない
- 社員IDが同じなら、同じ社員として判定できる。
- 社員IDが識別子となっている
- 同一かどうかを識別子で判定しているので、エンティティとして考えられる
値オブジェクトの例
- 通貨
- 通貨を金銭的価値だけで比べる場合に、2枚の千円札は製造年が異なっていても同じとみなされる。
- 属性が一致していれば同じであると判定しているので、値オブジェクトとして考えられる
ちなみに、ActiveRecordを用いたモデルのインスタンスは、
「id」を識別子としており、エンティティの実装に用いられます。
Railsでの値オブジェクトの活用例
「メールアドレス」という属性を持つUserモデルがあり、
そのUserが持つメールアドレスのドメイン名だけを返すロジックを追加したいとします。
Modelが肥大化しやすい実装
Userモデルのインスタンスメソッドとして、ロジックを実装
class User < ApplicationRecord
validates :email, format: { with: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/ }
def email_domain
email.split("@").last
end
end
この実装の問題点
- 今後、メールアドレスに関するロジックが増えると、Userモデルが肥大化してしまう。
- User以外にもメールアドレスを持つモデルが現れた時に、そのモデルにも同じメソッドを実装する必要がある
では次に、これらの問題を解決するために、値オブジェクトを用いてロジックを実装してみます。
値オブジェクトを用いた実装
メールアドレスを値オブジェクトとして、Userモデルから切り分け
Emailという値オブジェクトを導入し、
そのオブジェクトにメールアドレス属性とメールアドレスに関するロジックを実装します。
class Email
attr_reader :value
delegate :hash, to: :value
def initialize(value)
raise "Email is invalid" unless value.match?(/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/) # (*1)
@value = value.frozen? ? value : value.dup.freeze # (*2)
end
def ==(other)
self.class == other.class && value == other.value # (*3)
end
def domain
email.split("@").last
end
end
# (*1)
# バリデートの処理も、Userモデルから切り分けることができました
# (*2)
# 値オブジェクトが満たすべき特徴として、不変性があります
# そのため、オブジェクトが生成されてから、属性が変化しないようにしています。
# (*3)
# 値オブジェクトが満たすべき特徴として、等価性があります
# そのため、他のEmailオブジェクトと比較が行えるようにしています。
class User < ApplicationRecord
composed_of :email, mapping: %w[email value] # (*4)
end
# (*4)
# composed_ofは、 Railsのモデルで、値オブジェクトを扱いやすくするためのメソッド
# 詳しい説明はここではしないので、ぜひ調べてみてください。
メールアドレスを値オブジェクトとして、切り分けることで得られるメリット
- メールアドレスのロジックが増えても、Userモデルが肥大化しない
- メールアドレスに関するロジックを実装する場所が明確になる
- User以外のモデルでも、メールアドレスのロジックを再利用できる
さいごに
Railsでモデリングを行なう際、ActiveRecordを使う関係で、無理やりエンティティで実装しがちです。ドメインモデルをすべてエンティティで表現しようとすると、FatModelになる可能性大なので、エンティティと値オブジェクト、どちらで実装すべきなのかをきちんと吟味することが大切です!!
参考文献
パーフェクト Ruby on Rails
ValueObjectという考え方
3分でわかる値オブジェクト
DDD基礎解説:Entity、ValueObjectってなんなんだ
composed_of を使って Rails で値オブジェクトを扱う
メールアドレスを表す現実的な正規表現
明日
Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2020 19日目の記事は、いかがでしたでしょうか。
明日は @cheez921 さんの記事です!! ぜひ皆さん読んでください!!