【読書】DDDをドメイン駆動設計入門を読み始めた# 4章〜6章
すごいサボってました。 とはいえ、DDDは読み終わっているので、4章から6章のまとめになります。
4章 ドメインサービス
- 値オブジェクト・エンティティに記述すると、不自然な振る舞いをドメインサービスに記述する
- ドメインサービスを濫用すると、なんでもありのオブジェクトになりロジックが点在してしまう
- 基本的には、値オブジェクト・エンティティに振る舞いを記述することを考える
ドメイン駆動におけるサービスの役割
ドメインサービス
ドメインオブジェクト(値オブジェクト、エンティティ)には、ドメインのルールが記述される。
ドメインオブジェクトだけでは、ルールの記述が不自然な場合がある。
例えば、重複判定のコード
class User { private id: UserId private name: UserName constructor(id: UserId, name: UserName) { this.id = id this.name = name } // 重複検査を行うオブジェクト alreadyExists(user:User): Boolean { // Userテーブルの重複検査 } } // 呼び出しもとのアプリケーション const userId:UserId = new UserId(1) const userName:UserName = new UserName('hoge') const user:User = new User(userId, userName) // 重複検査 const isDuplicate:Boolean = user.Exists(user)
コードベースで考えると特に違和感がない
const user:User = new User(userId, userName) // 重複検査 const isDuplicate:Boolean = user.alreadyExists(user)
自分が重複しているか、自分自身に尋ねています。
alreadyExits
は、true
とfalse
をどちらを返すべきか、分かりにくい設計になっている。
(false
を返した時に、オブジェクト自体は生成されているのに存在しないっていう返し方は不自然。。)
ドメインオブジェクトだけでは、ドメインのルールを記載するのには限界がある時に利用するのがドメインサービス
になる。
ドメインサービスを利用してコードの改善
先ほどのエンティティに記述すると、不自然な感じになった重複検査を
ドメインサービスに閉じ込めると下のようなコードになる。
別のサービス層に処理を移譲したことで、ルールの不自然さが消える。
class User { private id: UserId private name: UserName constructor(id: UserId, name: UserName) { this.id = id this.name = name } } class UserService { constructor() {} alreadyExists(user:User): Boolean { // Userテーブルの重複検査 } } // 呼び出しもとのアプリケーション const userId:UserId = new UserId(1) const userName:UserName = new UserName('hoge') const user:User = new User(userId, userName) // 重複検査 const userService:UserService = new UserService() const isDuplicate:Boolean = userService.alreadyExists(user)
ドメインモデルの濫用は避ける
ドメインサービスに記載する振る舞いは、値オブジェクト・エンティティに記載すると、不自然なものに限定する。
(ぶっちゃけドメインサービス自体が、全ての振る舞いをなんでも記述できる)
値オブジェクトやエンティティに記述できる振る舞いを、ドメインサービス記述すると、
ロジックが点在して、改修やコードリーディングの際に精神的な疲労が発生します。
振る舞いを作成する際には、値オブジェクト・エンティティに実装することを考える。
5章 リポジトリ
リポジトリとは
- アプリケーションは、ドメインオブジェクトを繰り返す必要がある
- そのために、ドメインオブジェクトをデータストアに保存して、再構築する必要がある
- データストアへのCRUD処理を仲介して抽象的にするのがリポジトリ
値オブジェクト・エンティティから直接データストアに対して処理を行いことが大事 - オブジェクトが特定のデータストアにロックインされる - ドメインモデルにとって、データストアが何であっても保存して読み込みができればいい - 同じような処理をリポジトリに集約して、ロジックの点在を防止できる
Object(Entity) ↓↑ Repository リポジトリがデータストアへの処理を仲介する ↓↑ Data Store(RDB, NoSQL...)
リポジトリの責務
リポジトリの責務は、ドメインオブジェクトの永続化・再構築
を行うこと
そして、データストアに関わる処理をアプリケーションサービスやドメインに記述するとコードの可読性が悪くなる
さっきのUserServiceにDB関わる処理を加えてみると下のようなコードになる
SQLを見て初めて、id
をベースに重複判定していることがわかってくる
ドメインサービス以外(アプリケーションサービス)にも同じようなメソッドを書いていくと、
データストアのロジックが点在して、ビジネスロジックの部分が分かりにくくなる。
class UserService implements DbConfig { constructor() { } async alreadyExists(user:User): Boolean { const config:DbConfig = { host: 'hoge', user: 'hoge_user', password: 'hogehoge', database: 'hogedb' } try { let conn = await mysql.createConnection(config) let sql = mysql.format('SELECT EXISTS (SELECT * FROM users WHERE user_id = ?', user.id) let result = await conn.query(sql) return result.length > 0 } catch(error) { console.error("Error.") throw new Error(error) } finally { conn.close() } } }
データストアに対する処理をリポジトリに集約すると下のようなコードになる
データストアの処理でUserId
を引数にとることで、ドメインサービスをみるだけで、何を重複判定しているのか、
SQLを見なくてもわかるようになる。
途中でNoSQLが必要になった場合でも、新しくIUserRepository
を実装したリポジトリを用意することで、
ドメインサービスがデータストアへの依存から切り離すことができる。
interface IUserRepository { await exists(userId:UserId):boolean; } class UserRepository implements IUserRepository { private dbConfig:DbConfig constructor() { this.dbConfig = { host: 'hogehoge', user: 'hoge', password: 'password', database: 'fugafuga' } } await exists(userId): boolean { try{ let conn = await mysql.createConnection(this.dbConfig) let sql = mysql.format('SELECT EXISTS (SELECT * FROM users WHERE user_id = ?', userId) let result = await conn.query(sql) return result.length > 0 } catch(error) { throw new Error(error) } finally { conn.close() } } class UserService implements DbConfig { private userRepository:IUserRepository constructor() { this.userRepository = new UserRepository() } async alreadyExists(user:User): Boolean { try { const result = this.userRepository.exists(user.id) } catch(error) { console.error("Error.") throw new Error(error) } } }
リポジトリの注意点
データストアに関する処理をリポジトリに集約することで、ドメインの振る舞いを抽象化することできる。
ただ、ドメインの特定の要素を更新するような振る舞いをリポジトリに用意することはしてはいけない。
ユーザの要素が増えるごとにドメインに実装するメソッドが増えて、クソみたいなコードになる。
interface IUserRepository { await exists(userId:UserId):boolean; await updateUserName(userId: UserId, userName:UserName):User; await updateUserEmail(userId: userId, userEmail:userEmail):User: }
オブジェクトが保持するデータを更新するのであれば、オブジェクト自身が担当するか、update用のメソッドを1つ用意する方がいい。
6章 アプリケーションサービス
アプリケーションサービスの責務
「ユーザを登録する」とか「ユーザ情報を更新する」といったユースケースを実現するオブジェクト
アプリケーションサービスが、ユースケースに応じてドメインオブジェクトを呼び出す処理を行う。
class UserApplicationSerive { private userRepository:IUserRepository private userService:UserService constructor(userRepository: IUserRepository, userService: UserService) { this.userRepository = userRepository this.userService = userService } // 特定のユーザ取得 get(userId: string): User { const targetId = new userId(userId) const user = this.userRepository.find(targetId) return user } // ユーザの保存処理 }
ドメインサービスとアプリケーションサービス
ドメインサービスは、ドメインの知識を表現する(ドメインオブジェクトのために存在する)サービス層
「ユーザの重複確認」は、ドメインの知識(何を重複の判定条件にしているか)なので、ドメインオブジェクトに実装される。
アプリケーションサービスは、「ユーザを登録」などアプリケーションが成り立つために必要なサービス層
ドメインの知識に関わる処理は、アプリケーションサービスには実装されない。