본문 바로가기

Kotlin

Kotlin in Action 11장 : DSL 만들기

개요

  • labmda with receiver 를 이용한 DSL
  • invoke convention

코틀린의 이러한 특성을 이용하여 DSL을 어떻게 설계해나가는지 살펴보자

API에서 DSL로

API 가 깔끔하다?

  1. 적절한 이름을 통한 명확하게 이해할 수 있음?
  2. 코드의 간결성. 불필요한 구문, 번잡한 코드가 없어야 함 ( 11장에서 주로 볼 부분 )
  • extension function
  • infix calls
  • operator overloading
  • convention
  • lambda with receiver

Kotest - 코틀린 테스트

코틀린 테스트는 기존 Junit의 then 절의 가독성을 DSL과 함께 매우 높여주었다. DSL에서 중위호출을 어떻게 활용하는지 살펴보자

https://github.com/kotest/kotest

class KotestSample {

    @Test
    fun shouldKeyword() {
        val str: String = "start"

        str should startWith("sta") // infix call
        str should start with "sta" // = str.should(start).with("sta")
    }
}
  • 첫 should 함수는 kotest에서 제공해주는 것이다

should를 kotest에서 어떻게 구현했는지 살펴보자. should, startWith, neverNullMatcher 이 3가지를 살펴보다보니 generic, object, lambda 등 모든 내용을 이해하게 된다.

  •  
// kotest 구현 살펴보기
infix fun <T> T.should(matcher: Matcher<T>) {
   AssertionCounter.inc()
   val result = matcher.test(this)
   if (!result.passed()) {
      ErrorCollector.collectOrThrow(failure(result.failureMessage()))
   }
}


// StringMatcherKt.class
fun startWith(prefix: String) = neverNullMatcher<String> { value ->
  val ok = value.startsWith(prefix)
  var msg = "${value.show().value} should start with ${prefix.show().value}"
  val notmsg = "${value.show().value} should not start with ${prefix.show().value}"
  if (!ok) {
    for (k in 0 until min(value.length, prefix.length)) {
      if (value[k] != prefix[k]) {
        msg = "$msg (diverged at index $k)"
        break
      }
    }
  }
  MatcherResult(ok, msg, notmsg)
}

// MatcherResult.kt
      operator fun invoke(
         passed: Boolean,
         failureMessage: String,
         negatedFailureMessage: String
      ) = object : MatcherResult {
         override fun passed(): Boolean = passed
         override fun failureMessage(): String = failureMessage
         override fun negatedFailureMessage(): String = negatedFailureMessage
      }
      
// Matcher.kt
fun <T : Any> neverNullMatcher(test: (T) -> MatcherResult): Matcher<T?> {
   return object : NeverNullMatcher<T>() {
      override fun testNotNull(value: T): MatcherResult {
         return test(value)
      }
   }
}
  • 두번째 should 함수는 우리가 추가 구현한 것이다
object start

infix fun String.should(x: start): StartWrapper = StartWrapper(this)

class StartWrapper(val value: String) {
    infix fun with(prefix: String) =
        if (!value.startsWith(prefix))
            throw AssertionError("error")
        else Unit
}