前置き
ScalaでDDDを実践してみるにあたって検討したことをメモがてら書いていきます。今回はエンティティです。
エンティティをイミュータブルにする
エンティティについては他の言語では多くの場合ミュータブルなオブジェクトで表現されるようです。
そして、ドメインモデルとしてのエンティティオブジェクトの生成をプログラム上のインスタンス生成と一致させます。
値の変更はミュータブルに処理されるのでインスタンスは生成されません。
しかしながらScalaの基本はイミュータブルです。必要があれば部分的にミュータブルにも書けますが、ドメインオブジェクトという重要な部分がミュータブルになるのは避けたいものです。
イミュータブルにするということは値の変更があればインスタンスが生成されることになるため他言語での実装とは異なる部分が増えていきます。これからこの点について検討していきます。
生成の制限
エンティティをイミュータブルにするとつい case class
で表現したくなりますが、これは避けます。
DDDでは多くの場合コンストラクタまたはファクトリが生成をし、メソッドが変更を担当します。
case class
は自動でファクトリや、 copy
メソッドが生成されますが、これらは使用しないため不要です。
ここでは以下のようにします。
- 自分で定義したコンパニオンオブジェクトの
apply
メソッドか、ファクトリーオブジェクトを用いてエンティティの新規作成をする - 変更メソッドは変更された新しいインスタンスを生成する
- デフォルトコンストラクタは上記2つとリポジトリからのみ呼び出される
これにより各制約は以下のように書き分けます。
- 常に守らなければならない制約(不変条件)はコンストラクタ(つまりクラス定義本体)
- 新規作成時のみの制約はコンパニオンオブジェクトの
apply
- 変更時のみの制約は変更メソッド
デフォルトコンストラクタについてはアクセス制御すると良いでしょう。
例
上記をまとめると以下のようなコードになります。
class User private[model](
val id: UserId,
val name: Name
) {
// あるなら不変条件を書く
def updateName(newName: Name): User = {
// あるなら更新時の条件を書く
new User(id, newName)
}
}
// コンパニオンオブジェクトで生成する場合
object User {
def apply(id: UserId, name: Name): User = {
// 新規作成時のみの制約はここに書く
new User(id, name)
}
}
// DI等が必要でファクトリーを作る場合
class UserFactory(idProvider: IdProvider[UserId]) {
def apply(name: Name): User = {
// 新規作成時のみの制約はここに書く
new User(idProvider.get(), name)
}
}
2020/7/6 例を追記
2021/4/6 全体的に文章を見直し。ファクトリーに関して追記