Delegate Pattern
delegate란 말 그대로 '위임'하는 것을 말합니다.
코틀린에서는 by를 통해 어떠한 기능에 대한 수행을 자신이 수행하지 않고 다른 클래스에 위임할 수 있습니다. 이를 Delegate Pattern이라고 합니다.
상속과 합성
상속은 Is-a 관계라고 많이 이야기합니다. 예를 들어 animal 클래스와 animal 클래스를 상속받는 dog 클래스가 있을 때 dog is a animal 이라는 관계가 성립됩니다.
상속은 부모 클래스-자식 클래스 간의 의존성이 높습니다 . 부모 클래스의 변화는 자식 클래스에게도 영향을 미치게 되죠. 부모 클래스-자식 클래스의 관계가 컴파일 시점에 결정되기 때문에 구현을 변경할 수도 없습니다. 이와 같은 높은 결합도는 유연성과 확장성을 떨어뜨립니다. 또한 상속을 하면 부모 클래스에 대한 구현이 자식 클래스에 드러나게 됩니다. 이는 캡슐화를 깨뜨린다고 볼 수도 있습니다.
합성은 Has-a관계라고 볼 수 있습니다. 예를 들어 계산하는 cashier 클래스와 식당의 전체적인 관리를 맡는 manager 클래스가 있다고 합시다. 계산원이 자리를 비었을 때, manager는 cashier의 계산 기능을 가지고와 계산 할 수 있습니다. 즉, Has-a관계는 다른 클래스의 기능을 가지고와 사용합니다.
객체 합성을 사용하면 객체의 내부를 공개하지 않고, 내용이 변경되어도 영향을 최소화할 수 있어 안정적입니다. 상속과는 다르게 상위 클래스에 의존하지 않고, 런타임에 동적으로 변경할 수 있으니 유연한 설계가 가능해집니다.
상속을 지양하고, 합성을 지향하라는 말이 있는데요. dog is a animal 처럼 Is-a관계가 명확한 경우 상속을 사용하고, 그렇지 않은 경우는 합성을 사용할 것을 권장한다고 합니다.
by를 사용한 위임
한 가지 상황을 예를 들어 보겠습니다.
한 직원(me) 이 상사로부터 프린터를 해오길 부탁받습니다. 직원은 프린트를 할 수 있는 방도가 없기에, 프린트를 해주는 가게 A, B를 찾아 맡기기로(위임) 합니다. 직원은 가게B에서 프린트를 하고 상사에게 제출합니다.
interface Ability {
fun print()
}
class StoreA(): Ability {
override fun print() {
println("가게A: 저렴하지만, 화질이 떨어지는 프린트 완료")
}
}
class StoreB(): Ability {
override fun print() {
println("가게B: 비싸지만, 화질이 좋은 프린트 완료")
}
}
class Employee(private val store: Ability): Ability by store {
fun submit() {
println("팀장님 프린트 완료했습니다.")
}
}
fun main(args: Array<String>) {
val me = Employee(StoreB())
me.print()
me.submit()
}
인터페이스를 통해 코드를 재사용하기 때문에 StoreB() 클래스에 있는 오버라이딩 되지 않은 함수는 사용이 불가능 합니다.
class StoreB(): Ability {
override fun print() {
println("가게B: 비싸지만, 화질이 좋은 프린트 완료")
}
fun scan() {
println("가게B: 스캔 완료")
}
}
fun main(args: Array<String>) {
val me = Employee(StoreB())
me.print()
me.scan() // Error
me.submit()
}
만약 상속을 사용하면 어떨까요?
interface Ability {
fun print()
}
class StoreA(): Ability {
override fun print() {
println("가게A: 저렴하지만, 화질이 떨어지는 프린트 완료")
}
}
class StoreB(): Ability {
override fun print() {
println("가게B: 비싸지만, 화질이 좋은 프린트 완료")
}
}
class Employee(private val store: Ability): Ability {
override fun print() {
store.print()
}
fun submit() {
println("팀장님 프린트 완료했습니다.")
}
}
fun main(args: Array<String>) {
val me = Employee(StoreB())
me.print()
me.submit()
}
Ability 인터페이스를 상속받은 Employee 클래스가 프로퍼티로 받은 객체의 메서드 store.print() 를 호출하는 것을 볼 수 있습니다. Ability 인터페이스에 스캔, 제본 등 여러 기능이 추가 된다면 Employee가 오버라이드 메서드는 늘어나고, 이미 선언된 함수를 똑같이 작성하는 것이나 다름이 없습니다. by를 사용하여 위임한다면 보일러 플레이트 코드를 줄일 수 있습니다.
observable 위임자
observable을 사용하면 프로퍼티의 데이터가 변할 때 로직을 수행합니다. 프로퍼티 변경에 대한 로그를 찍을 때 유용하게 사용할 수 있을 것 같습니다.
import kotlin.properties.Delegates
class User(val name: String) {
var address: String by Delegates.observable("None") {
property, oldValue, newValue -> println("${name}의 주소가 [${oldValue} => ${newValue}]로 변경되었습니다.")
}
}
fun main(args: Array<String>) {
val user = User("김코딩")
user.address = "경기도 광주시"
user.address = "서울특별시"
}
vetoable 위임자
vetoable을 사용하면 프로퍼티를 변경할 경우 특정 조건에 맞는지 판단하여 값을 변경할 수 있고, 변경하지 않을 수 있습니다. 아래 코드에서는 value 프로퍼티를 변경하려고 할 경우 oldValue < newValue 조건에 맞다면 값을 변경합니다.
초기값이 5인 객체 bigger가 있을 때, bigger의 value 프로퍼티를 4로 변경하려고 하면 oldValue 값이 더 크기 때문에 값이 변경되지 않습니다. 5보다 큰 7로 프로퍼티 값을 변경하려고 하면 조건에 맞기 때문에 값이 변경되는 것을 확인할 수 있습니다.
import kotlin.properties.Delegates
class Bigger(initValue: Int) {
var value: Int by Delegates.vetoable(initValue) {
property, oldValue, newValue ->
println("old = $oldValue | new = $newValue")
oldValue < newValue
}
}
fun main(args: Array<String>) {
val bigger = Bigger(5)
bigger.value = 4
println(bigger.value)
bigger.value = 7
println(bigger.value)
}
다음과 같이 짝수인 경우에만 값을 변경하도록 작성할 수도 있습니다.
import kotlin.properties.Delegates
fun main(args: Array<String>) {
var even:Int by Delegates.vetoable(0) {
property, oldValue, newValue ->
println("old = $oldValue | new = $newValue")
newValue % 2 == 0
}
even = 3
println(even)
even = 4
println(even)
}
'Android > Kotlin' 카테고리의 다른 글
[코틀린 인 액션] 8장 정리: 고차 함수 (0) | 2022.12.13 |
---|---|
[코틀린 인 액션] 7장 정리: 연산자 오버로딩 (0) | 2022.12.05 |
[코틀린 인 액션] 6장 정리: null 다루는 방법 (0) | 2022.11.30 |
[코틀린 인 액션] 5장 정리 (0) | 2022.11.30 |
[코틀린 인 액션] 4장 정리 (0) | 2022.11.29 |