【DDD】基本概念と実装例の紹介
DDD(ドメイン駆動開発)の本を読んだので、そのアウトプット用の記事です。
DDDの基本的な概念とその実装例を紹介していきます。
値オブジェクト
システム固有の値を表したオブジェクト。
例)商品在庫管理システムの商品番号など
data class ProductNumber private constructor(val value: String) {
companion object {
fun create(value: String): ProductNumber {
if (value.isEmpty()) {
throw IllegalArgumentException("商品番号は空にできません")
}
if (!isValidFormat(value)) {
throw IllegalArgumentException("商品番号のフォーマットが不正です: $value")
}
return ProductNumber(value)
}
private fun isValidFormat(number: String): Boolean {
return number.matches(Regex("^[A-Z]{3}-\\d{5}$"))
}
}
}
特徴
- 不変: 内部状態が変更されない
- 交換可能: 不変の特徴を持つため、値の変更がはできないが、インスタンスごと変更はできる
- 等価性による比較: 同じ属性値を持つ二つの値オブジェクトは等しいとみなされる
エンティティ
同一性(あるオブジェクトが他のオブジェクトと区別される固有の特性)によって比較され、ライフサイクルがある。
「人」をエンティティとして考えると、Aさんは名前が変わったり、住所が変わったりしても「同一の人物」。また、Aさんと名前と住所が同じBさんがいたとしても、その二人は別の「人」として区別される。
例) ユーザー、注文、商品など
class User private constructor(
val id: UserId,
name: Name,
email: Email
) {
var name = name
private set
var email = email
private set
fun changeName(newName: Name) {
this.name = newName
}
fun changeEmail(newEmail: Email) {
this.email = newEmail
}
// 同一性の比較(equals, hashCodeのオーバーライド)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is User) return false
return id == other.id // IDだけで比較する
}
override fun hashCode(): Int {
return id.hashCode()
}
}
特徴
- 同一性による比較: エンティティは内部の属性値が変わっても、IDなどの識別子が同じであれば同一のエンティティとみなされる
- 可変: エンティティはライフサイクルを通じて状態が変化することが許される
ドメインサービス
値オブジェクトやエンティティの振る舞いとして定義すると不自然な操作や、複数のエンティティや値オブジェクトを跨ぐ操作が必要な場合に使用するオブジェクト。
例) ユーザー名やメールアドレスの重複チェックなど
class UserDomainService(private val userRepository: UserRepository) {
/**
* ユーザー名の重複チェック
*/
fun isExistsUserName(name: Name): Boolean {
return userRepository.findByName(name) != null
}
}
使用する上での注意点
可能な限りドメインサービスを使うことは避ける。オブジェクトのすべての振る舞いをドメインサービスに書くことができてしまうため、不自然な振る舞いのみをドメインサービスとして切り出すことが重要。
リポジトリ
エンティティなどのドメインオブジェクトの永続化と再構築を担当するオブジェクト。
実装例)ユーザーリポジトリ
interface UserRepository {
fun findById(id: UserId): User?
fun findAll(): List<User>
fun save(user: User)
}
class UserRepositoryImpl : UserRepository {
override fun findById(id: UserId): User? {
// 実装を書く
}
override fun findAll(): List<User> {
// 実装を書く
}
override fun save(user: User){
// 実装を書く
}
}
集約
関連性の強いオブジェクト群をひとつの単位として扱うことで、データの整合性を守るための仕組み。
集約ルート(AggregateRoot)が存在し、集約に対する操作はすべて集約ルートを経由して行う。
リポジトリでの永続化や復元は集約単位で行う。
例)ユーザー集約
plantuml
@startuml user
package "ユーザー集約" {
class User <<AggregateRoot>> {
-UserId id
-UserName name
-Email email
+changeName(UserName)
+changeEmail(Email)
}
class UserId <<ValueObject>> {
-UUID value
}
class UserName <<ValueObject>> {
-String value
}
class Email <<ValueObject>> {
-String value
}
User *-- UserId
User *-- UserName
User *-- Email
}
@enduml
// 以下のような集約ルートを経由しない変更はNG
user.name = "名前"
user.email = "test@mail.com"
// OK
user.changeName("名前")
user.changeEmail("test@mail.com")
ファクトリ
複雑なオブジェクトの生成を担当するオブジェクト。エンティティの生成が複雑な場合やリポジトリからデータを取得し、その結果を元にエンティティを構築する必要がある場合に有効。
例)ユーザーファクトリ(リポジトリの結果を元にエンティティを構築したいケース)
class UserFactory(
private val userIdRepository: UserIdRepository
) {
/**
* 自動採番されたユーザーIDを使用してユーザーを作成する
*/
fun createUser(name: UserName, email: Email): User {
// システム的に採番された次のユーザーIDを取得
val nextUserId = userIdRepository.getNextUserId()
return User(
id = nextUserId,
name = name,
email = email,
status = UserStatus.PENDING,
createdAt = LocalDateTime.now()
)
}
}
アプリケーションサービス
ユースケースを実現するサービス。ドメインオブジェクト(エンティティ、値オブジェクト、ドメインサービスなど)を組み合わせて、アプリケーションの機能実装を担当。
例)ユーザー登録
class UserRegisterApplicationService(
private val userRepository: UserRepository,
private val userFactory: UserFactory,
private val userRegisterValidationDomainService: UserRegisterValidationDomainService
) {
/**
* ユーザー登録ユースケース
*/
fun execute(userName: String, email: String): UserId {
val name = UserName.create(userName)
val userEmail = Email.create(email)
userRegisterValidationDomainService.validate(name, userEmail)
val user = userFactory.createUser(name, userEmail)
userRepository.save(user)
return user.id
}
}
参考
Discussion