필자는 Fluent API DSL 보다 Kotlin Type safe builder 를 활용한 DSL 을 좋아하는데, 보기에 좀더 깔끔하기 때문이다.
Flunet API 는 Chaining 신경써야 하고 Depth 파악이 더 어렵기 때문
그러나 Fluent API의 경우 Class Method Chaining 을 기반으로 하기 때문에 Class base 로 제약조건을 걸기가 좀더 쉽다.
예를들어, Name Age Email 을 받아서 User 를 만드는 Builder 를 설계한다고 해보자.
제약 조건은 다음과 같다.
1. age 는 Optional
2. Name -> Email 순으로 받아야 하며, Required Field 이다 (일종의 Step)
물론 해당 조건은 Compile Time 에 해당 제약 조건이 적용 되어야 한다 (exception 으로 처리하는것이 아님)
러프하게 다음과 같이 작성 할 수 있다
interface NameStep {
fun setName(name: String): EmailStep
fun setAge(age: Int): NameStep
}
interface EmailStep {
fun setEmail(email: String): OptionalStep
fun setAge(age: Int): EmailStep
}
interface OptionalStep {
fun setAge(age: Int): OptionalStep
fun build(): User
}
class UserFluentBuilder : NameStep {
private var name: String = ""
private var age: Int? = null
override fun setName(name: String): EmailStep {
this.name = name
return UserEmailBuilder(name=name, age=age)
}
override fun setAge(age: Int): NameStep {
this.age = age
return this
}
}
class UserEmailBuilder(private var name: String, private var age: Int?) : EmailStep {
private var email : String = ""
override fun setEmail(email: String): OptionalStep {
this.email = email
return OptionalBuilder(name=name, age=age, email=email)
}
override fun setAge(age: Int): EmailStep {
this.age = age
return this
}
}
class OptionalBuilder(private var name:String,private var age: Int?, private var email:String) : OptionalStep {
override fun setAge(age: Int): OptionalStep {
this.age = age
return this
}
override fun build(): User {
return User(name, email, age)
}
}
fun main() {
val user1 = UserFluentBuilder()
.setName("User")
.setEmail("user1@mail.com")
.setAge(20)
.build()
println(user1)
}
fluent api 는 그냥 일반적인 class 의 chaining과 동일 하므로, 그냥 그렇게 되도록 짜면 된다.
문제는 이것을 kotlin type safe builder 식으로 구현 하고 싶을때인데, 이게 조금 까리한것이,
우리가 원하는것은 age/name/email 을 동일한 Line 에 구현 하고 싶다는 것이다.
// 이런 느낌으로 만들고 싶다
user {
age = 10
name = "seeroe"
email = "dldnjs1013@nate.com"
}
// 이런 느낌은 싫다!
user {
name ("seeroe") {
age (10) {
email("dlndjs1013@nate.com")
}
}
}
하지만 fluent api 를 그대로 kotlin type 식으로 표현하면 아래와 같은 하이어리키를 가지게 된다. 꼴보기 싫다.
요것이 까리한 이유이다..
같은 Depth 내에서
1. age 는 Optional
2. Name -> Email 순으로 받아야 하며, Required Field 이다(일종의 Step)
위 조건을 만족하는 Kotlin DSL 을 구현하기 어려운 이유는, Depth 구분을 하지 않으면 리턴타입 표현이 안되고 Required 여부를 강제하는 방법도 딱히 없기 때문이다.
대충 Generic 과 Sealed class 에 확장메서드 + where generic 조건도 고민해보고
타 랭기지의 monad notation 이 flatmap chain 을 flat 하게 해주니까 Receivers vs. flatMap | Arrow (arrow-kt.io) 을 보면서 적용할수 있지 않을까 고민도 많이 해봣는데
원하는 코드대로는 절대 나오지 않아서 열심히 구글링한 결과.. 비슷한 내용을 얻을 수 있었다. (허풍쟁이 AI는 이럴때 쓸모가 없다)
https://stackoverflow.com/questions/53651519/how-to-make-field-required-in-kotlin-dsl-builders
어마어마한 보일러 플레이트 코드가 생기긴 하는데, 뭐 여튼..
방법은 확장 메서드 + Where 절로 타입 제약을 걸고 "contract" 를 이용하는것으로 스마트 케스팅을 유도한다 (왜 이 생각을 못햇지?!)
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
data class User(val name: String, val email: String, val age: Int?)
sealed class UserBuilder {
var age: Int? = null
interface Named {
var name: String
}
interface Emailed {
var email: String
}
private class Impl : UserBuilder(), Named, Emailed {
override lateinit var name: String
override lateinit var email: String
}
companion object {
operator fun invoke(): UserBuilder = Impl()
}
}
@OptIn(ExperimentalContracts::class)
fun UserBuilder.name(name: String) {
contract {
returns() implies (this@name is UserBuilder.Named)
}
(this as UserBuilder.Named).name = name
}
@OptIn(ExperimentalContracts::class)
fun <S> S.email(email: String) where S:UserBuilder, S:UserBuilder.Named {
contract {
returns() implies (this@email is UserBuilder.Emailed)
}
(this as UserBuilder.Emailed).email = email
}
fun <S> S.build(): User where S : UserBuilder, S : UserBuilder.Named, S:UserBuilder.Emailed =
User(name, email, age)
fun user(init: UserBuilder.() -> User) = UserBuilder().init()
fun main() {
val builder = UserBuilder()
builder.age = 25
builder.name("John Doe")
builder.email("dldnjs1013@nate.com")
builder.build()
val u = user {
name("seeroe")
email("dldnjs1013@nate.com")
age = 25
build()
}
println(u)
}
'프로그래밍 언어 노트 > JAVA | Kotlin' 카테고리의 다른 글
Behavioral Programming 라이브러리를 코틀린에서 구현해보자 feat Channel (0) | 2024.10.14 |
---|---|
[Ktor] + kts 활용해서 Dynamic Endpoint 만들기 (1) | 2024.07.22 |
Kotlin 초기화 코드 제너레이터 (0) | 2024.07.16 |
코틀린으로 살펴보는 Y 컴비네이터 (Feat Fixed Point) (1) | 2024.07.15 |
Hibernate Query Plan 으로 인한 OOM 방지 (0) | 2024.06.24 |