다재다능 코틀린 프로그래밍 책으로 코틀린 스터디를 진행하면서 발표를 위해 준비했던 글입니다.
- '개는 동물이다'와 같은 확실한 IS-A 관계일 때만 상속을 사용하고 그외엔 델리게이션을 사용하자.
- 코틀린은 델리게이션을 사용할 때 중복 코드 작성을 예방해준다.
델리게이션을 사용한 디자인
- Java를 사용한 델리게이션의 문제점
- DRY(Don't Repeat Yourself)원칙 위반: 위임하는 객체의 메소드가 늘어날 수록 위임을 사용하는 클래스의 중복 코드가 늘어난다. 메소드 내부에서 메소드명이 거의 비슷한 델리게이션 객체 메소드를 호출하는 형태들
- OCP(Open-Closed Principle)원칙 위반: 위임하는 객체가 수정되면 위임을 사용하는 클래스도 수정되어야한다.
- 언어의 지원이 부족하기 때문에 생기는 문제점
코틀린의 by 키워드를 사용한 델리게이션
- 가장 간단한 형태의 델리게이션
interface Worker {
fun work()
}
class JavaProgrammer: Worker {
override fun work() {
println("write java")
}
}
class CProgrammer: Worker {
override fun work() {
println("write C")
}
}
class Manager() : Worker by JavaProgrammer()
val manager = Manager()
manager.work()
- Manager 클래스는 JavaProgrammer 클래스를 상속받지 않는다.
- JavaProgrammer 타입 참조가 필요한 곳에 사용하면 오류 발생
- 컴파일러가 내부적으로 Manager 클래스에 JavaProgrammer 메소드를 생성하고 요청한다.
- 다시 말해서 직접 구현하지 않아도 코틀린 컴파일러에 의해 합성된다.
파라미터에 위임하기
- 위에서 살펴본 가장 간단한 델리게이션 형태의 문제점
- JavaProgrammer 인스턴스에만 요청이 가능, 다른 종류는 불가
- Manager 클래스 내부에서는 델리게이션한 객체에 접근이 불가능하다.
- 생성자에 델리게이션 파라미터를 전달함으로써 해결 가능
class Manager(val staff: Worker) : Worker by staff {
fun meeting() = println("meeting with ${staff.javaClass.simpleName}")
}
val javaManager = Manager(JavaProgrammer())
val CManager = Manager(CProgrammer())
- 델리게이션할 객체를 파라미터에서 val로 받으면 해당 객체의 속성이 되기 때문에 클래스 내부 meeting 메소드에서 접근이 가능하다.
- val로 받지 않으면 속성이 되지 않고 그냥 델리게이션으로 사용한다.
메소드 충돌 관리
- 델리게이션을 사용하는 클래스는 코틀린 컴파일러가 랩퍼를 만든다.
- 델리게이션을 사용하는 클래스에서 델리게이션 객체와 메소드 시그니처가 같은 메소드를 구현하는 경우엔 override 키워드로 우선순위를 부여해 충돌을 해결한다.
class Manager(val staff: Worker) : Worker by staff {
override fun work() {
println("manager work")
}
}
- 여러 개의 인터페이스를 델리게이션하는 경우 메소드 충돌
class Manager(val staff: Worker, val assistant: Assistant) :
Worker by staff, Assistant by assistant {
override fun eat() {
assistant.eat()
}
}
- 델리게이션하는 인터페이스들 중 메소드 시그니처가 같은 메소드가 있다면 override 하지 않으면 컴파일 에러가 난다.
- override로 충돌을 방지해줘야하고 override한 메소드에서 수동으로 어떤 위임 객체를 사용할 지 선택하면 된다.
델리게이션 주의사항
- 델리게이션의 진짜 목적은 Manager가 Worker를 이용하는 것, 하지만 코틀린의 부작용으로 Manager 타입이 Worker 타입의 한 종류가 된다.
interface Worker {
fun work()
}
class JavaProgrammer() : Worker {
override fun work() {
print("work")
}
}
class Manager(val staff: Worker) : Worker by staff {
}
val coder : JavaProgrammer = Manager() // 에러
val manager : Worker = Manager(JavaProgrammer()) // 가능
- 델리게이션을 var로 사용할 때의 주의 사항
interface Worker {
fun work()
}
class JavaProgrammer() : Worker {
override fun work() {
println("java work")
}
}
class CProgrammer() : Worker {
override fun work() {
println("C work")
}
}
class Manager(var staff: Worker) : Worker by staff {
}
val manager = Manager(JavaProgrammer())
manager.work()
manager.staff = CProgrammer()
manager.work()
- 위 코드의 출력 결과를 예상과 다르게 work() 두 번 다 "java work"를 출력한다.
- 델리게이션을 사용하는 객체는 두 개의 참조가 있다.
- 백킹 필드로서 존재하는 참조
- 델리게이션의 목적으로 존재하는 참조
- 객체를 생성할 때 staff란 이름의 멤버에 파라미터로 받은 객체를 할당한다. (마치 this.staff = staff)
- 속성을 변경해도 필드만 변경되고 델리게이션의 참조가 변경되지 않는다.
- 뿐만 아니라, 원래 사용하던 JavaProgrammer 인스턴스에 접근할 수 없게 된다.
변수와 속성 델리게이션
변수 델리게이션
- 속성이나 지역변수를 읽을 때, 코틀린 내부에서는 getValue() 함수를 호출
- 속성이나 변수를 설정할 때, 코틀린 내부에서는 setValue() 함수를 호출
- 객체의 델리ㄱ이션을 위의 두 메소드와 함께 제공함으로써 객체의 속성과 지역변수를 읽고 쓰는 요청을 가로챌 수 있다.
import kotlin.reflect.KProperty
var comment = "This is Stupid"
println(comment) // This is stupid
class PoliteString(var content: String) {
operator fun getValue(thisRef: Any?, property: KProperty<*>) =
content.replace("stupid", "s*****")
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
content = value
}
}
var comment2: String by PoliteString("some message")
println(comment2) // some message
comment2 = "This is stupid"
println(comment2) // This is s*****
속성 델리게이션
import kotlin.reflect.KProperty
class PoliteString(val dataSource: MutableMap<String, Any>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>) =
(dataSource[property.name] as? String)?.replace("stupid", "s*****") ?: ""
operator fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
dataSource[property.name] = value
}
}
class PostComment(dataSource: MutableMap<String, Any>) {
val title: String by dataSource
var likes: Int by dataSource
val comment: String by PoliteString(dataSource)
override fun toString() = "Title: $title Like: $likes Comment: $comment"
}
val postComment = PostComment(
mutableMapOf(
"title" to "Using Delegation",
"likes" to 1,
"comment" to "stupid"
)
)
postComment.likes++
println(postComment.toString())
// Title: Using Delegation Like: 2 Comment: s*****
- MutableMap도 getValue(), setValue()를 가지고 있기 때문에 델리게이션을 사용할 수 있다.
- 속성을 다른 객체에게 위임할 수 있고 내부적으로 필드를 저장하고 있을 수도 있다.
빌트인 스탠다드 델리게이션
지연 델리게이션
- lazy 랩퍼 함수 사용하기
fun getTemperature(city: String) : Double {
println("this $city")
return 30.0
}
val city = "seoul"
var isShow = false
val temperature by lazy { getTemperature(city) }
if (isShow && temperature > 20) println("warm") else println("cold")
- lazy 랩퍼 함수를 사용해서 결과가 진짜 필요할 때 까지 식을 실행하지 않도록 지연 연산을 요청할 수 있다.
- 식 결과가 어떠한 경우에도 필요하지 않으면 식 전체를 스킵해 버린다.
- 람다 표현식이 실행되면 델리게이션은 결과를 저장하고 있다가 미래에 요청이 있으면 저장된 값을 알려준다.
- 람다 표현식이 다시 실행되는게 아님
observable 델리게이션
- observable() 메소드를 사용하면 변화를 지켜볼 수 있다.
import kotlin.properties.Delegates.observable
var count by observable(0) { property, oldValue, newValue ->
println("property: $property oldValue: $oldValue newValue: $newValue")
}
println("count is $count")
count++
println("count is $count")
// count is 0
// property: ....count: kotlin.Int oldValue: 0 newValue: 1
// count is 1
- observable 핸들러로 등록하면 모니터링과 디버깅 목적으로 사용할 때 매우 유용하다.
vatoable 델리게이션
- 지켜보기만 하는 게 아니라 변경을 허가할건지 말건지를 정하려면 vetoable 델리게이션을 사용하자.
import kotlin.properties.Delegates.vetoable
var count by vetoable(0) { _, oldValue, newValue -> newValue > oldValue}
println("count is $count")
count++
println("count is $count")
count--
println("count is $count")
// count is 0
// count is 1
// count is 1
- vetoable 핸들러를 등록하면 Boolean 결과를 리턴받을 수 있다. true 허가 / false 불허
'스터디 > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글
11. 내부 반복과 지연 연산 (0) | 2021.11.29 |
---|---|
10. 람다를 사용한 함수형 프로그래밍 (0) | 2021.11.29 |
8. 클래스 계층과 상속 (0) | 2021.11.29 |
7. 객체와 클래스 (0) | 2021.11.29 |
6. 오류를 예방하는 타입 안전성 (0) | 2021.11.29 |