안드로이드 개발자 노트
[이펙티브 코틀린] Item35. 복잡한 객체를 생성하기 위한 DSL을 정의하라 본문
코틀린은 DSL(Domain Specific Language)을 직접 만들 수 있습니다.
DSL은 복잡한 객체, 계층 구조를 갖고 있는 객체들을 정의할 때 유용합니다.
DSL을 만드는 것은 힘든 일이지만, 한 번 만들고 나면 보일러플레이트(boilerplate)와 복잡성을 숨기면서 개발자의 의도를 명확하게 표현할 수 있습니다.
// HTML DSL
body {
div {
a("https://kotlinlang.org") {
target = ATarget.blank + "Main site"
}
}
+"some content"
}
// Andriod View DSL (Anko 라이브러리)
verticalLayout {
val name = editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!")}
}
}
// Test DSL
class MyTests: StringSpec({
"반환되는 길이는 String의 크기이어야 합니다." {
"test string".length shouldBe 5
}
"startsWith 함수는 prefix를 반환해야 합니다." {
"world" should startWith("wor")
}
})
// Gradle DSL
plugins {
'java-library'
}
dependencies {
api("junit:junit:4.12")
implementation("junit:junit:4.12")
testImplementation("junit:junit:4.12")
}
configurations {
implementation {
resolutionStrategy.failOnVersionConflict()
}
}
사용자 정의 DSL 만들기
사용자 정의 DSL을 만들려면, 리시버를 사용하는 함수 타입에 대한 개념을 이해해야 합니다.
함수 타입은 함수로 사용할 수 있는 객체를 나타내는 타입입니다.
예를 들어 filter함수를 살펴보면, predicate에 함수 타입이 활용되고 있습니다.
inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
val list = arrayListOf<T>()
for (elem in this) {
if (predicate(elem)) { list.add(elem) }
}
return list
}
함수 타입의 몇 가지 예를 살펴보면 다음과 같습니다.
- ()->Unit : 인자가 없고, Unit을 리턴합니다. (반환값 없음)
- (Int)->Unit : Int 인자를 받고, Unit을 리턴합니다. (반환값 없음)
- (Int)->Int : Int 인자를 받고, Int를 리턴합니다.
- (Int,Int)->Int : Int, Int 총 2개를 받고, Int를 리턴합니다
- (Int)->()->Unit : Int 인자를 받고, ()->Unit 타입인 다른 함수를 리턴합니다.
- (()->Unit)->Unit : ()->Unit 타입인 함수를 인자로 받고, Unit을 리턴합니다. (반환값 없음)
함수 타입을 만드는 기본적인 방법은 다음과 같습니다.
- 람다 표현식
- 익명 함수
- 함수 레퍼런스
// val name: type = ...
val plusLambda: (Int, Int)->Int = { a, b -> a + b }
val plusAnonymous: (Int, Int)->Int = fun(a, b) = a + b
val plusReference: (Int, Int)->Int = ::plus
// 타입 추론
val plusLambda2 = { a: Int, b: Int -> a + b }
val plusAnonymous2 = fun(a: Int, b: Int) = a + b
코틀린에는 확장함수가 존재합니다. 확장함수는 아래와 같이 만들 수 있습니다.
fun Int.myPlus(other: Int) = this + other
확장 함수도 익명 함수로 만들 수 있으며, 이를 리시버를 가진 함수 타입이라고 부릅니다.
val myPlus: Int.(Int)->Int = fun Int.(other: Int) = thie + other
val myPlus = fun Int.(other: Int) = thie + other
확장 함수에서 리시버는 this 키워드로 참조할 수 있습니다.
val myPlus: Int.(Int)->Int = { this + it }
리시버를 가진 익명 확장 함수와 람다 표현식은 다음과 같은 방법으로 호출할 수 있습니다.
- 일반적인 객체처름 invoke 메서드를 사용
- 확장 함수가 아닌 함수처럼 사용
- 일반적인 확장 함수처럼 사용
myPlus.invoke(1, 2)
myPlus(1, 2)
1.myPlus(2)
리시버를 가진 함수 타입의 가장 중요한 특징은 this의 참조 대상을 변경할 수 있다는 것입니다.
this는 apply 함수에서 리시버 객체의 메서드와 프로퍼티를 간단하게 참조할 수 있게 해 주기도 합니다.
inline fun <T> T.apply(block: T.() -> Unit): T {
this.block()
return this
}
class User {
var name: String = ""
var surname: String = ""
}
val user = User().apply {
name = "Marcin"
surname = "jiwon"
}
언제 사용해야 할까?
DSL을 정의한다는 것은 개발자의 인지적 혼란과 성능이라는 비용이 모두 발생할 수 있습니다.
DSL은 다음과 같은 것을 표현하는 것에 유용합니다.
- 복잡한 자료 구조
- 계층적인 구조
- 거대한 양의 데이터
정리
- DSL은 언어 내부에서 사용할 수 있는 특별한 언어다.
- 복잡한 객체는 물론이고 HTML 코드, 복잡한 설정 등 계층 구조를 갖는 객체를 간단하게 표현할 수 있게 해준다.
- DSL이 익숙하지 않은 개발자에게 혼란과 어려움을 줄 수 있다.
- 하지만 잘 정의된 DSL은 프로젝트에 큰 도움을 준다.
'Kotlin > 이펙티브 코틀린' 카테고리의 다른 글
[이펙티브 코틀린] Item36. 상속보다는 컴포지션을 사용하라 (0) | 2023.12.30 |
---|---|
[이펙티브 코틀린] Item34. 기본 생성자에 이름 있는 옵션 아규먼트를 사용하라 (0) | 2023.12.24 |
[이펙티브 코틀린] Item33. 생성자 대신 팩토리 함수를 사용하라 (0) | 2023.12.17 |