Rails で DDD をしている時の集約の実装方法を考えます。
ActiveRecord と集約を別として考える
メリット
- 集約が
PORO (Plain Old Ruby Object、素の Ruby)
で実装できる - 集約にDB へのアクセスロジックが混在せず、ビジネスロジックの実装に集中できる
デメリット
-
ActiveRecord
のインスタンスが別に作成されるのでメモリの使用効率が悪い - 集約へ再構成を行う実装コストがかかる (ライブラリがあるかも)
方法
ActiveRecord
のインスタンスは DAO として捉えます。
DAO はテーブルに対して操作をおこなうインタフェースとして考えればよく、リポジトリから使用することが想定されます。
例えば、作成や更新を行う リポジトリ#save
を実装する場合には以下のようになります。
class UserRepository
def save(user)
user_dao = User.find_or_initialize_by(identity: user.identity)
user_dao.assign_attributes(
name: user.name,
email: user.email
)
user_dao.save!
end
end
次に、リポジトリ#user_of_identity
というメソッドを実装して、識別子から集約のオブジェクトを検索/再構成するパターンです。
class UserRepository
def user_of_identity(identity)
user_dao = User.find_by(identity: identity)
return nil unless user_dao
user = Users::User.allocate # 集約を再構成する
user.instance_variable_set(:@name, user_dao.name)
user.instance_variable_set(:@email, user_dao.email)
user
end
end
ポイントは再構成を行う時に、Class#allocate
を使用していることです。
このメソッドはクラスのインスタンスを作成しますが、#initialize
を呼び出しません。
再構成時に #initialize
の呼び出しを回避することで、オブジェクトの作成時にのみドメインイベント発行させたりすることが出来ます。
また、前述の理由のため、集約の各属性への値の代入も、集約を振る舞いを使用せずに #instance_variable_set
を使用して行います。
実際には、再構成の処理をリポジトリのプライベートメソッドや、Rails のモデルにロジックをまとめる方法が有効的です。
番外編: ActiveRecord と集約を同じにする
メリット
- リポジトリにて、集約への再構成を行うコストが少なくなる
- メモリの使用効率が良い
- composed_of で値オブジェクトをマッピング出来る
デメリット
- ビジネスロジックと DB へのアクセスロジックが混在する
- DB アクセスのロジックを別モジュールにしてファイルを分けることは出来そう
- ドメインモデル層に
ActiveRecord
のインタフェースが公開されている状態になる
まとめ
基本的には、DB へのアクセスロジックは集約には漏らさないようにしたいので ActiveRecord
と集約を別にするパターンで良いのかなと思いました。高い非機能要件が求められて効率的にメモリを使用しなければならない場合には、ActiveRecord
と集約を同じにするパターンを検討してみてはいかがでしょうか。
ただし、高速化を求める方法はキャッシュの活用や、クエリのチューニングなどでも大きな効果があるはずなので、本当の最終手段のような気がしました。