일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라 #
프로퍼티 위임을 사용하면 일반적인 프로퍼티 행위를 추출해서 재사용할 수 있다. 또, 프로퍼티 위임 메커니즘을 통해 다양한 패턴들을 만들 수 있다.
프로퍼티 위임은 프로퍼티 패턴을 추출하는 일반적인 방법이라 많이 사용되고 있다.
lazy, observable, 뷰, 리소스 바인딩, 의존성 주입, 데이터 바인딩 등의 사용 예시가 있겠다.
// 예시 : lazy
val value by lazy { createValue() }
// 예시 : observable
var items: List<Item> by
Delegates.observable(listof()) { _, _, _ ->
notifyDataSetChanged()
}
// 예시 : 뷰, 리소스 바인딩 (안드로이드)
private val button: Button by bindView(R.id.button)
private val textSize by bindDimension(R.dimen.font_size)
private val doctor: Doctor by argExtra(DOCTOR_ARG)
// 예시 : 의존성 주입 (Koin)
private val presenter: MainPresenter by inject()
private val repository: NetworkRepository by inject()
private val vm: MainViewModel by viewModel()
// 예시 : 데이터 바인딩
private val port by bindConfiguration("port")
private val token: String by preferences.bind(TOKEN_KEY)
커스텀 프로퍼티 위임 만들어보기 #
프로퍼티 위임은 다른 객체의 메서드를 활용해서 프로퍼티의 접근자(getter/setter)를 만드는 방식이다.
게터, 세터 사용 시 로그를 출력하는 예시가 있다고 가정해보자.
var token: String? = null
get() {
print("token returned value $field")
return field
}
set(value) {
print("token changed from $field to $value")
field = value
}
var attempts: Int = 0
get() {
print("attempts returned value $field")
return field
}
set(value) {
print("attempts changed from $field to $value")
field = value
}
위 코드를 프로퍼티 위임을 통해 개선해보자. (확장 함수로 만들 수도 있다.)
var token: String? by LoggingProperty(null)
var attempts: Int by LoggingProperty(0)
// 위에서 말한 *다른 객체의 메서드를 사용한다* 는 부분을 기억하자.
// 확장 함수로 만들어도 된다.
private class LoggingProperty<T>(var value: T) {
operator fun getValue(
thisRef: Any?,
prop: KProperty<*>
): T {
print("${prop.name} returned value $value")
return value
}
operator fun setValue(
thisRef: Any?,
prop: KProperty<*>,
newValue: T
) {
val name = prop.name
print("$name changed from $value to $newValue")
value = newValue
}
}
private class LoggingProperty<T>(var value: T) {
operator fun getValue(
thisRef:
)
}
getValue, setValue 메서드가 여러 개 있어도 무방하다. (오버로딩 형태로 생각해면 될까 싶다.)
위의 코드 중 by
가 어떻게 컴파일되는지 살펴보자.
// var token: String? by LoggingProperty(null)
// var attempts: Int by LoggingProperty(0)
@JvmField
private val 'token$delegate' =
LoggingProperty<String?>(null)
var token: String?
get() = 'token$delegate'.getValue(thisRef = this, prop = ::token)
set(value) {
'token$delegate'.setValue(
thisRef = this,
prop = ::token,
newValue = value
)
}
- val 의 경우, getValue 가 필요하다.
- var 의 경우, getValue, setValue 가 필요하다.
그 외 예시 #
val map: Map<String, Any> = mapOf(
"name" to "Marcin",
"programmer" to true
)
val name by map
print(name) // Marcin
위 코드는 코틀린 stdlib 에 확장 함수가 정의되어 있기에 사용할 수 있는 코드다.
inline operator fun <V, V1 : V> Map<in String, V>.getValue(thisRef: Any?, property: KProperty<*>): V1 =
getOrImplicitDefault(property.name) as V1
코틀린 stdlib의 다음과 같은 프로퍼티 위임을 알아 두면 좋다.
- lazy
- Delegates.observable
- Delegates.vetoable
- Delegates.notNull
굉장히 범용적으로 사용되는 델리게이터들이다.
참고 : https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/
참고 : notNull #
var max: Int by Delegates.notNull()
// println(max) // will fail with IllegalStateException
max = 10
println(max) // 10
참고 : observable #
var observed = false
var max: Int by Delegates.observable(0) { property, oldValue, newValue ->
observed = true
}
println(max) // 0
println("observed is ${observed}") // false
max = 10
println(max) // 10
println("observed is ${observed}") // true
참고 : vetoable #
값 변경 시 특정 조건에 따라 변경을 취소할 수 있다.
inline fun <T> vetoable(
initialValue: T,
crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean
): ReadWriteProperty<Any?, T>
(source)
var max: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
newValue > oldValue
}
println(max) // 0
max = 10
println(max) // 10
max = 5
println(max) // 10