반응형
본 포스트는 지인들과 스터디한 내용을 정리한 포스트입니다
http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788931463422
1. 델리게이션을 통한 확장
- 상속과 델리게이션 모두 객체지향 프로그래밍의 디자인 방식이다
- 두 방식 모두 클래스를 다른 클래스로부터 확장시킨다
- 상속과 델리게이션 차이
- 상속은 베이스 클래스(부모)에 강력하게 묶이고 수정할 수 없게 된다
- 델리게이션은 객체 자신이 처리해야 할 일을 다른 클래스의 인스턴스에게 위임하거나 넘겨버릴 수 있다
- Java는 상속에 대해 많은 지원을 해줬지만 델리게이션에 대해서는 지원이 약하다
2. 상속 대신 델리게이션을 써야 하는 상황
- 클래스의 객체가 다른 클래스의 객체가 들어갈 자리에 쓰여야 한다면 상속을 사용해라
- 클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야 한다면 델리게이션을 사용해라
- 상속의 단점
- 정적 타입 객체지향 언어에서의 상속을 통한 다형성의 매력이라고 생각하고 사용한다
- 베이스 클래스에서 상속받은 인스턴스를 자식 클래스에서 마음대로 바꾸려는 행동은 오류를 일으킬 수 있다
- 리스코프 치환원칙(LSP)
- 자식 클래스에서 부모 클래스의 메소드를 오버라이드할 때 베이스 클래스의 외부 동작을 유지해아하는 문제가 있다 ( 제약사항 )
- 클래스의 구현을 새로 하거나 한 클래스의 인스턴스를 '개는 동물이다'와 같이 포함 관계에 있는 다른 클래스로 대체할 때 상속을 사용하라
- 오직 다른 객체의 구현을 재사용하는 경우라면 델리게이션을 사용하라
3. 델리게이션을 사용한 디자인
- 기존 Java의 델리게이션 디자인
// project.kts interface Worker { fun work() fun takeVacation() } class JavaProgrammer : Worker { override fun work() { println("...write Java...") } override fun takeVacation() { println("...code at the beach...") } } class CSharpProgrammer : Worker { override fun work() { println("...write C#...") } override fun takeVacation() { println("...branch at the ranch...") } } class Manager(private val worker: Worker) { fun work() = worker.work() fun takeVacation() = worker.takeVacation() } val javaProgrammer = JavaProgrammer() val manager = Manager(javaProgrammer) manager.work() // ...write Java... manager.takeVacation() // ...code at the beach...
- Manager는 단순히 호출의 기능만 수행한다
- Worker에 더 많은 기능이 들어가면 또 Manager에 더 많은 호출 코드가 들어간다
- DRY(Don't Repeat Yourself) 원칙을 위반하는 나쁜 디자인이다
- 또한 OCP(개방 폐쇄 원칙)도 위반했는데, 변경에는 닫혀있어야하고, 확장에는 열려있어야하는 원칙에서 봤을 때 Worker가 변경되면 Manager 또한 코드변경이 불가피하므로 좋지 않은 디자인이다
- 코틀린의 by 키워드를 사용한 델리게이션
// projectWithDeligation.kts interface Worker { fun work() fun takeVacation() } class JavaProgrammer : Worker { override fun work() { println("...write Java...") } override fun takeVacation() { println("...code at the beach...") } } class CSharpProgrammer : Worker { override fun work() { println("...write C#...") } override fun takeVacation() { println("...branch at the ranch...") } } class Manager() : Worker by JavaProgrammer() val doe = Manager() doe.work() // ...write Java... doe.takeVacation() // ...code at the beach...
- Worker by JavaProgrammer() 를 보면 by라는 키워드를 통해 간단히 인터페이스로 요청을 위임하는 델리게이션을 구현했다
- by 키워드가 컴파일 시간에 이전 예제에서 우리가 시간을 들여서 수동으로 구현했던 델리게이션을 대신 해준다
- Manager 클래스는 JavaProgrammer 클래스를 상속받은 것이 아니다
- 코틀린의 델리게이션을 사용하면, 컴파일러가 내부적으로 Manager 클래스에 메소드를 생성하고 요청을 한다
4. 파라미터에 위임하기
- 위에서 보았던 by 키워드를 이용해 객체를 생성하면서 델리게이션하는 방식은 문제점이 있다
- Manager 클래스의 인스턴스는 오직 JavaProgrammer의 인스턴스에만 요청할 수 있다 ( 다른 종류의 Worker에는 요청 불가능 )
- Manager의 인스턴스는 델리게이션에 접근할 수 없다 ( Manager 클래스 안에 다른 메소드를 작성하더라도 해당 메소드에서는 델리게이션에 접근할 수 없다 )
- 인스턴스를 생성하면서 델리게이션을 지정하지 않고 생성자에 델리게이션 파라미터를 전달함으로써 해결이 가능하다
// projectWithDeligationAnother.kts interface Worker { fun work() fun takeVacation() } class JavaProgrammer : Worker { override fun work() { println("...write Java...") } override fun takeVacation() { println("...code at the beach...") } } class CSharpProgrammer : Worker { override fun work() { println("...write C#...") } override fun takeVacation() { println("...branch at the ranch...") } } class Manager(val staff : Worker) : Worker by staff { fun meeting() = println("organizing meeting with ${staff.javaClass.simpleName}") } val doe = Manager(CSharpProgrammer()) doe.work() // ...write C#... doe.takeVacation() // ...branch at the ranch... doe.meeting() // organizing meeting with CSharpProgrammer val roe = Manager(JavaProgrammer()) roe.work() // ...write Java... roe.takeVacation() // ...code at the beach... roe.meeting() // organizing meeting with JavaProgrammer
- Manager 클래스는 staff 파라미터를 델리게이션으로 사용한다
- 위와 같은 델리게이션은 하나의 클래스에 국한되지 않는다 ( 유연성 )
- Worker 클래스의 메소드를 호출하면 코틀린이 자동으로 연결된 델리게이션으로 요청을 전달한다
5. 메소드 충돌 관리
- 코틀린 컴파일러는 델리게이션에 사용되는 클래스마다 델리게이션 메소드를 위한 랩퍼를 만든다
- 코틀린에서는 델리게이션을 이용하는 클래스가 델리게이션 클래스의 인터페이스를 구현해야한다
- 하지만 실제로는 인터페이스의 각 메소드를 모두 구현하지 않는다
- 델리게이션 클래스가 인터페이스의 메소드를 이미 구현한 상태인 경우 델리게이션을 이용하는 클래스에서 다시 메소드를 구현하려고할땐 override 키워드를 사용해아한다
// projectWithDeligationOverride.kts interface Worker { fun work() fun takeVacation() } class JavaProgrammer : Worker { override fun work() { println("...write Java...") } override fun takeVacation() { println("...code at the beach...") } } class CSharpProgrammer : Worker { override fun work() { println("...write C#...") } override fun takeVacation() { println("...branch at the ranch...") } } class Manager(val staff : Worker) : Worker by staff { override fun takeVacation() = println("of course") } val doe = Manager(CSharpProgrammer()) doe.work() // ...write C#... doe.takeVacation() // of course val roe = Manager(JavaProgrammer()) roe.work() // ...write Java... roe.takeVacation() // of course
- 코틀린은 메소드 충돌을 훌륭하게 해결해 인터페이스의 특정 메소드를 델리게이션으로 요청하지 않도록 오버라이드 해준다
- 두개의 인터페이스를 구현하는 경우
// projectWithDeligationDuplicate interface Worker { fun work() fun takeVacation() fun fileTimeSheet() = println("Why? Really?") } class JavaProgrammer : Worker { override fun work() { println("...write Java...") } override fun takeVacation() { println("...code at the beach...") } } class CSharpProgrammer : Worker { override fun work() { println("...write C#...") } override fun takeVacation() { println("...branch at the ranch...") } } interface Assistant { fun doChores() fun fileTimeSheet() = println("No escape from that") } class DepartmentAssistant : Assistant { override fun doChores() = println("routine stuff") } class Manager(private val staff: Worker, private val assistant: Assistant) : Worker by staff, Assistant by assistant { override fun takeVacation() = println("of course") override fun fileTimeSheet() { print("manually forwarding this...") assistant.fileTimeSheet() } } val doe = Manager(CSharpProgrammer(), DepartmentAssistant()) doe.work() // ...write C#... doe.takeVacation() // of course doe.doChores() // routine stuff doe.fileTimeSheet() // manually forwarding this...No escape from that
6. 델리게이션 주의사항
- 지금까지의 예제를 예로 Manager 클래스는 JavaProgrammer를 사용할 수 있지만 Manager를 JavaProgrammer로 사용할 수는 없다
- 예제코드
// projectVersion8.kts interface Worker { fun work() fun takeVacation() fun fileTimeSheet() = println("Why? Really?") } class JavaProgrammer : Worker { override fun work() = println("...write java...") override fun takeVacation() = println("...code at the beach...") } class CSharpProgrammer : Worker { override fun work() = println("...write C#...") override fun takeVacation() = println("...branch at the ranch...") } class Manager(var staff: Worker) : Worker by staff val doe = Manager(JavaProgrammer()) println("Staff is ${doe.staff.javaClass.simpleName}") // Staff is JavaProgrammer doe.work() // ...write java... println("changing staff") // changing staff doe.staff = CSharpProgrammer() // Staff is CSharpProgrammer println("Staff is ${doe.staff.javaClass.simpleName}") // ...write java... doe.work()
- 위에서 선언된 델리게이션은 속성이 아니라 파라미터이다
- doe.staff = CSharpProgrammer()로 처음에 선언된 Manager 클래스의 staff 프로퍼티 참조를 바꿔주어도 필드만 변경한 것이지 델리게이션의 참조를 변경한 것이 아니다
- 위 같은 코드는 또다른 문제를 발생시키는데, 기존의 JavaProgrammer의 인스턴스에 더 이상 접근할 수가 없어졋다(필드를 CSharpProgrammer로 변경했기 때문에), 하지만 델리게이션이 JavaProgrammer를 사용중이기 때문에 가비지컬렉터가 수집해가지도 않는다...
- 코틀린은 객체의 속성이 아닌 주 생성자에 보내진 파라미터로 델리게이션을 한다
7. 변수와 속성 델리게이션
- 속성이나 지역변수를 읽을 때, 코틀린 내부에서는 getValue() 함수를 호출한다
- 속성이나 변수를 설정할 때 코틀린은 setValue() 함수를 호출한다
- 변수 델리게이션
- 지역변수의 읽기와 쓰기에 대한 접근을 모두 가로챌 수 있다
- 리턴 되는 것을 변경할 수 있고 데이터를 언제, 어디에 저장하는지도 변경할 수 있다
- 예제코드(클래스)
// PoliteString.kt package chapter9 import kotlin.reflect.KProperty 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 } }
- 예제코드(스크립트)
// politecomment.kts import chapter9.PoliteString var comment: String by PoliteString("Some nice message") println(comment) // Some nice message comment = "This is stupid" println(comment) // This is s***** println("comment is of length: ${comment.length}") // comment is of length: 14
- 코틀린에선 인터페이스를 구현할 필요도 없고 관습적인 코드도 없다, 단지 메소드만 있으면 된다
- 메소드 시그니처에 대한 확신이 없다면 kotlin.properties.ReadOnlyProperty와 kotlin.properties.ReadWriteProperty 인터페이스를 참고하면 된다
- 속성 델리게이션
- 델리게이션은 getValue()를 구현하는(읽기전용), getValue()와 setValue()를 모두 사용하는(읽기-쓰기) 속성이라면 어떤 객체에서든 사용 가능하다
- 코틀린 스탠다드 라이브러리의 디자인을 보면 Map과 MutableMap은 델리게이션을 사용할 수 있다
- Map은 val 속성, MutableMap은 var 속성으로 사용할 수 있다 ( Map은 get(), getValue() 보유, MutableMap은 set(), setValue()까지 보유 )
- 예제코드
// postcomment.kts import kotlin.reflect.KProperty class PoliteString(val dataSource: MutableMap<String, Any>) { operator fun getValue(thisRef: Any?, property: KProperty<*>): String = (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(): String = "Title: $title Likes: $likes Comment: $comment" } val data = listOf( mutableMapOf( "title" to "Using Delegation", "likes" to 2, "comment" to "Keep it simple, stupid"), mutableMapOf( "title" to "Using Inheritance", "likes" to 1, "comment" to "Prefer Delegation where possible" ) ) val forPost1 = PostComment(data[0]) val forPost2 = PostComment(data[1]) forPost1.likes++ println(forPost1) // Title: Using Delegation Likes: 3 Comment: Keep it simple, s***** println(forPost2) // Title: Using Inheritance Likes: 1 Comment: Prefer Delegation where possible
- 빌트인 스탠다드 델리게이션
- 코틀린은 우리가 실제로 유용하게 사용할 만한 몇 가지 빌트인 델리게이션을 제공해준다
- 지연 델리게이션: 객체 생성이나 연산 실행이 실제 필요할 때까지 실행을 지연시켜주는 유용한 기능
- observable 델리게이션: 속성의 값이 변하는 것을 지켜보게 해주는 유용한 기능
- Vetoable 델리게이션: 기본 규칙이나 비즈니스 로직에 기반한 속성이 변경되는 것을 막아줌
- 지연 델리게이션
- 개발자는 직접 컴파일러에게 식의 결과가 정말로 필요하기 전까지는 식을 실행하지 않도록 지연 연산을 요청할 수 있다
- 식의 결과가 필요하지 않으면 식 전체를 스킵해 버린다
- lazy 래퍼 함수를 이용한 예제코드
// lazyevaluation.kts fun getTemperature(city: String): Double { println("fetch from webservice for $city") return 30.0 } val showTemperature = false val city = "Boulder" val temperature by lazy { getTemperature(city) } if (showTemperature && temperature > 20) // (nothing here) println("Warm") else println("Nothing to report")
- lazy 함수는 연산을 실행할 수 있는 람다 표현식을 아규먼트로 받는다, 그 후 필요한 순간에만 실행한다(요청 즉시 실행 X )
- lazy 함수는 기본적으로 람다 표현식의 실행과 동기화된다, 따라서 하나의 스레드만 실행된다
- 옵저버블 델리게이션
- 연관된 변수나 속성의 변화를 가로채는 ReadWriteProperty 델리게이션을 만든다
- 변화가 발생하면 델리게이션이 개발자가 observable() 함수에 등록한 이벤트 핸들러를 호출한다
- 예제코드
// observe.kts import kotlin.properties.Delegates var count by Delegates.observable(0) { property, oldValue, newValue -> println("Property: $property old: $oldValue: new: $newValue") } println("The value of count is: $count") count++ println("The value of count is: $count") count-- println("The value of count is: $count")
- 지역변수나 객체의 속성의 변화를 지켜보려고 한다면 옵저버블 델리게이션을 사용하라
- 모니터링과 디버깅 목적으로 사용할 때 매우 유용하다
- 지켜보기만 하는 게 아니라 변경을 허가할지, 불허할지 결정하려면 vetoable 델리게이션을 사용하도록 하자
- 코틀린은 우리가 실제로 유용하게 사용할 만한 몇 가지 빌트인 델리게이션을 제공해준다
728x90
반응형
'Study > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글
Day 10. 람다를 사용한 함수형 프로그래밍 (0) | 2021.12.09 |
---|---|
Day 8. 클래스 계층과 상속 (0) | 2021.11.16 |
Day 7. 객체와 클래스 (0) | 2021.11.12 |
Day 6. 오류를 예방하는 타입 안정성 (0) | 2021.11.02 |
Day 5. 컬렉션 사용하기 (0) | 2021.10.31 |