個人的な備忘録
レイヤードアーキテクチャでアプリケーションを作っていて、
賛否両論あると思うがドメイン層のバリデーションを作成時とDBからの再構成時にチェックしていて、
作成時は例外をそのまま投げたいが、再構成時はそうしたくないと思っていたので、表題のようなことをしたかった
(変なデータが入ったりしなければこんなことをしなくても良いが、特殊なケースで起こったりする為、モヤモヤしていたので調べていた)
例えば、↓みたいなクラスをドメインとして持っていて、 init
でバリデーションを行っているとする
バリデーションに失敗した場合は、 DomainException
をスローする
data class UserId(val value: UUID) { companion object { fun generate(): UserId { return UserId(UUID.randomUUID()) } } } data class User( val id: UserId, val name: UserName, ) { companion object { fun of( name: UserName, ): User { return User( id = UserId.generate(), name = name, ) } } } data class UserName(val value: String) { companion object { private const val MIN_LENGTH = 1 private const val MAX_LENGTH = 256 fun of(value: String): UserName { return UserName(value) } } init { if (MIN_LENGTH > value.length || MAX_LENGTH < value.length) { throw DomainException( message = "ユーザ名は${MIN_LENGTH}以上${MAX_LENGTH}以下の長さである必要があります", ) } } }
リポジトリのインターフェースはとりあえず保存とID検索だけ用意する
interface UserRepository { fun findById(id: UserId): User fun save(user: User) }
実装は以下
@Repository class UserRepositoryImpl: UserRepository { private val user1 = UserEntity( id = UUID.fromString("a687b0bc-0887-483c-8851-4604657162c1"), name = "", // なんかしらの理由で元々入っていたとする ) private val user2 = UserEntity( id = UUID.fromString("bb1304ea-8d78-4b99-b4d1-ce4000c1c9ab"), name = "テストユーザ", ) private val map = ConcurrentHashMap( mapOf( Pair( user1.id, user1, ), Pair( user2.id, user2, ), ) ) override fun findById(id: UserId): User { return map.getOrElse(id.value) { throw RuntimeException("ユーザが存在しません") } .toUser() } override fun save(user: User) { val entity = UserEntity( id = user.id.value, name = user.name.toString(), ) if(map.putIfAbsent(user.id.value, entity) != null) { throw RuntimeException("ユーザがすでに存在します") } } }
今回はDBを使ってないが、DBを使った場合を想定して、DB用のモデルを用意する
ここでDBのモデルからドメインモデルの変換をするが、バリデーションに失敗した場合は DomainException
がスローされる
data class UserEntity ( val id: UUID, val name: String, ) { fun toUser(): User { return User( id = UserId(value = id), name = UserName(value = name), ) } }
モデルの変換をするのは @Repository
のアノテーションがついたクラスなので、
AOPを使って @Repository
内のメソッドで DomainException
が投げられたら DomainMappingException
に変換して投げ直すことにした
あとはそれぞれの例外に応じて、例外ハンドラーを設定すればいい感じに作成時と再構成時で振る舞いを変えられる
@Aspect @Component class RepositoryExceptionTranslator { @AfterThrowing(value = "@within(org.springframework.stereotype.Repository)", throwing = "e") fun translate(e: Throwable) { throw when(e) { is DomainException -> DomainMappingException(e) else -> e } } }
こういうときはやっぱりAOPが便利だなといった感想
試していたソースのコードは↓