델리게이션을 통한 확장

·2021년 12월 13일
0
post-thumbnail

📌 상속 대신 델리게이션을 써야 하는 상황


상속은 객체지향 언어에서 흔하고, 많이 사용되는 최고의 기능이다. 델리게이션이 더 유연하지만, 많은 객체지향 언어들은 별로 지원을 안해준다.
코틀린은 델리게이션과 상속 모두를 지원하기 때문에 문제를 기반으로 적절한 해법을 선택하기만 하면 된다.

✔ 클래스의 객체가 다른 클래스의 객체가 들어갈 자리에 쓰여야 한다면 상속을 허용해라
✔ 클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야 한다면 델리게이션을 사용해라

CloudScape

candidate 클래스가 BaseClass를 상속 받을 때 Candidate클래스의 인스턴스는 내부에 BaseClass의 인스턴스를 같이 다닌다고 볼 수 있다. 베이스 인스턴스는 자식 클래스와 분리시킬 수 없을 뿐만 아니라 변경할 수도 없다.
베이스 클래스에서 상속받은 인스턴스를 자식 클래스에서 마음대로 바꾸려는 행동은 오류를 일으킬 수 있다. 리스코프 치환원칙이 경고하듯 말이다.
문제는 자식 클래스에서 부모 클래스의 메소드를 오버라이드할 때 베이스 클래스의 와부 동작을 유지해야 한다는 것이다. 상속을 사용해 자식클래스를 설계하면 엄청난 제약사항이 따른다.

Candidate 클래스가 델리게이션을 하면 Candidate 클래스의 인스턴스는 델리게이션 참조를 갖는다. 상속과 다르게 인스턴스들은 분리가 가능하고, 그 덕분에 엄청난 유연성을 가지게 된다. Caller는 Candidate에게 요청을 보내고, Candidate는 적절하게 해당 요청을 한다.

🔥 개발자들은 두 가지 중에서 현명한 선택을 해야 한다

클래스의 구현을 새로 하거나 한 클래스의 인스턴스를 "개는 동물이다"와 같이 포함관계에 있는 다른 클래스로 대체할 때 상속을 사용해라. 오직 다른 객체의 구현을 재사용하는 경우라면 델리게이션을 사용하라. 예를 들면 여러 Assistant를 가지고 있는 Manager 같은 경우를 말한다.

📌 델리게이션을 사용한 디자인


상속이 아닌 델리게이션을 쓰는 이유를 이해하기 위해서 상속을 이용해서 작은 문제를 디자인해보자.

디자인적 문제점

worker

interface Worker {
    fun work()
    fun takeVacation()
}

작업자는 두 개의 일을 할 수 있다. ✔ 첫째 일을 한다 ✔ 둘째 가끔 휴가를 떠난다.

worker

class JavaProgrammer : Worker {
    override fun work() = println("..write java..")
    override fun takeVacation() = println("..code at the beach.")

}

class CShareProgrammer : Worker {
    override fun work() = println("..write C#..")

    override fun takeVacation() = println("..branch at the ranch..")

}

프로그래머들은 각자 많은 일을 한다. work() 메소드는 각 클래스에 기반해 작동하고, takeVacation() 메소드를 통해 휴가를 간다.

Manager

class Manager

팀을 관리하기 위한 개발 매니저도 만들었다.

잘못된 경로로의 상속

Manager가 work()를 호출하기 위해서는 상속을 이용하면 된다. Manager를 JavaProgrammer에서 상속받으면 Manger 클래스에서 구현을 다시 작성할 필요가 없다.

JavaProgrammer

open class JavaProgrammer : Worker {
    override fun work() = println("..write java..")
    override fun takeVacation() = println("..code at the beach.")

}

JavaProgrammer 클래스에 open 표기를 사용한다.

Mannager

class Manager : JavaProgrammer()

Manager 클래스는 JavaProgrammer를 상속받았다.

val doe = Manager()
doe.work() // ..write java..

이제 Manager 인스턴스에서 work()를 사용할 수 있다.

🔥 잘 동작하긴 하지만 이 디자인에는 문제점이 존재한다.

  1. Manager 클래스는 JavaProgrammer 클래스에 갇혀버리게 된다.
    Manager 에서는 CShareProgrammer 클래스가 제공하는 구현을 사용할 수 없다.

  2. Manager가 JavaProgrammer나 특정 프로그래머에 종속되는 것을 의도하지 않았지만, 상속이 그렇게 만들었다.
val corder: JavaProgrammer = doe

모든 Worker에게 Manager가 의존하는 것을 원했지만 이건 상속에서 불가능하다. 하지만 델리게이션에서는 가능하다.

어려운 델리게이션

비록 코드가 코틀린이지만 잠시동안 Java에서 사용 가능한 기능만 사용해 보도록 하자.

Mannager

class Manager(val worker: Worker) {
    fun work() = worker.work()
    fun takeVacation() = worker.work()
}

val doe = Manager(JavaProgrammer())
doe.work()

Manager 인스턴스를 만든 후 JavaProgrammer 인스턴스를 생성자로 전달했다.
Manager의 생성자에 CShareProgrammer 클래스의 인스턴스 혹은 Worker 인터페이스를 구현한 클래스의 인스턴스라면 어떤 인스턴스든 넘길 수 있다.
또한 JavaProgrammer 클래스가 더 이상 상속을 해주지 않기 때문에 JavaProgrammer 클래스에 더 이상 open을 입력할 필요가 없다.

❗ 하지만 이런 디자인은 몇가지 문제가 있다.

  1. DRY(Don't Repeat Yourself) 원칙을 위반한다.
    Worker에 더 많은 메소드가 있다고 생각해보면 Manager에는 더 많은 호출 코드가 들어가야 할 것이다. 모든 호출 코드는 호출할 때 메소드명을 제외하고 거의 비슷하다.
  2. 개방-폐쇄 원칙을 위반한다
    클래스를 확장하기 위해서는 클래스를 변경해서는 안된다. 지금 구현된 코드를 보면 Worker 인터페이스에 deploy() 메소드를 추가한다면 Manager 클래스도 변경하여 해당 메소드를 호출시키는 메소드를 추가해야한다.

이런 문제로 인해 Java 개발자들은 델리게이션 보다 상속을 사용하려 하지만 코틀린은 이런 문제를 해결하기 위해 언어 수준에서 델리게이션을 지원한다.

코틀린의 by 키워드를 사용한 델리게이션

코틀린에서는 개발자가 직접 손대지 않고도 컴파일러에게 코드를 요청하도록 할 수 있다.

by 키워드

class Manager() : Worker by JavaProgrammer()

Manager는 JavaProgrammer을 이용해 Worker 인터페이스를 구현하고 있다.
코틀린 컴파일러는 Worker에 속하는 Manager 클래스의 메소드를 바이트코드 수준에서 구현하고, by 키워드 뒤에 나오는 JavaProgrammer 클래스의 인스턴스로 호출을 요청한다.

코틀린의 by 키워드의 왼쪽에는 인터페이스가, 오른쪽엔 해당 인터페이스를 구현한 클래스가 필요하다.

val doe = Manager()
doe.work() // ..write java..

상속과 비슷해 보이지만 몇 가지 주요한 차이점이 있다.

  1. Manager 클래스는 JavaProgrammer 클래스를 상속받지 않는다.
val corder: JavaProgrammer = doe //Error : Type mismatch: inferred type is Project.Manager but Project.JavaProgrammer was expected
  1. 상속을 사용한 솔루션에서 work() 같은 메소드를 호출하는 것은 Manager 클래스에서는 구현되지 않았다. 대신 베이스 클래스로 요청을 넘겼다.

📌 파라미터에 위임하기


이전 예제의 코드에는 두 가지 이슈가 있다.
첫 번째, Manager 클래스의 인스턴스는 오직 JavaProgrammer의 인스톤스에만 요청할 수 있다.
두 번째, Manager의 인스턴스는 델리게이션에 접근할 수 없다. Manager 클래스 안에 다른 메소드를 작성하더라도 해당 메소드에서는 델리게이션에 접근 할 수 없다는 의미이다.

💡 이런 제약은 인스턴스를 생성하면서 델리게이션을 지정하지 않고 생성자에게 델리게이션 파라미터를 전달함으로써 쉽게 해결 가능하다.

by 키워드

class Manager(val staff: Worker) : Worker by staff {
    fun meeting() = println("organizing meeting with ${staff.javaClass.simpleName}")
}

Manager 클래스의 생성자는 staff 라는 파라미터를 받는다. staff을 val로 정의했기 때문에 속성이 된다.
Manger 클래스의 meeting() 에서 staff에 접근 할 수 있다. staff은 Manager 객체의 속성이기 때문이다.

val doe = Manager(CShareProgrammer())
val roe = Manager(JavaProgrammer())
doe.work()
doe.meeting()
roe.work()
roe.meeting()

💻 출력

..write C#..
organizing meeting with CShareProgrammer
..write java..
organizing meeting with JavaProgrammer

델리게이션은 JavaProgrammer 같은 하나의 클래스에 구속되지 않고 Worker인터페이스를 구현한 클래스 모두 사용할 수 있다.

📌 메소드 충돌 관리


코틀린 컴파일러는 델리게이션에 사용되는 클래스마다 델리게이션 메소드를 위한 랩퍼를 만든다.
사용한느 클래스와 델리게이션 클래스에 동일한 이름과 시그니처가 있는 메소드가 있다면 어떻게 될까?

이전 예제에서 Worker 인터페이스는 takeVacation() 메소드를 가지고 있고, Manager 클래스는 해당 메소드를 델리게리션인 Worker에 위임했다.

코틀린에서는 델리게이션을 이용하는 클래스가 델리게이션 클래스의 인터페이스를 구현해야 한다.
하지만 실제로는 각 메소드를 모두 구현하지 않았다. Manager는 Worker의 인터페이스를 구현하지만 work(), takeVacation() 메소드를 실제로 구현하여 제공하지 않는다.

델리게이션 클래스의 모든 인터페이스를 위해서 코틀린 컴파일러가 랩퍼를 만든다. 델리게이션 클래스가 인터페이스의 메소드를 구현하지 않은 경우 델리게이션을 이용하는 클래스에서 메소드를 구현해야한다. 구현 한 상태에서 구현하는 경우에는 override 키워드를 사용해야 한다.

class Manager(val staff: Worker) : Worker by staff {
    override fun takeVacation() = println("of sourse")
}

컴파일러는 takeVacation() 메소드의 랩퍼를 생성하지 않고 work() 메소드의 랩퍼만 생성할 것이다.

val doe = Manager(CShareProgrammer())
doe.work()
doe.takeVacation()

💻 출력

..write C#..
of sourse

Manager 클래스의 인스턴스에서 work() 메소드를 사용하기 위해서 델리게이션으로 요청이 넘어갔다. 하지만 takeVacation() 메소드를 사용할 땐 델리게이션에세 요청하지 않고 Manager에 구현된 메소드가 실행되었다.

여러 인터페이스를 구현하는 클래스에서 인터페이스 사이에 메소드 충돌이 일어나는 경우를 알아보자.

메소드 충돌

interface Worker {
    fun work()
    fun takeVacation()
    fun fileTimeSheet() = println("Why? Really?")
}

interface Assistant {
    fun doChores()
    fun fileTimeSheet() = println("No escape from that")

}

class JavaProgrammer : Worker {
    override fun work() = println("..write java..")
    override fun takeVacation() = println("..code at the beach.")

}

class CShareProgrammer : Worker {
    override fun work() = println("..write C#..")

    override fun takeVacation() = println("..branch at the ranch..")

}

class DepartmentAssistant : Assistant {
    override fun doChores() = println("routine stuff")

}

구현

class Manager(val staff: Worker, val assistant: Assistant) : Worker by staff, Assistant by assistant {
    override fun takeVacation() = println("of course")
    override fun fileTimeSheet() {
        println("manually forwarding this...")
        assistant.fileTimeSheet()
    }

}

val doe = Manager(CShareProgrammer(), DepartmentAssistant())
doe.work()
doe.takeVacation()
doe.doChores()
doe.fileTimeSheet()

💻 출력

..write C#..
of course
routine stuff
manually forwarding this...
No escape from that

Manager 클래스에서 fileTimeSheet() 메소드를 오버라이드 하지 않는다면 메소드 충돌 때문에 컴파일 오류가 난다. fileTimeSheet() 메소드의 호출은 Manager 인스턴스에서 실행되었다. Manager 인스턴스가 fileTimeSheet() 메소드의 호출을 가로챘기 때문에 Worker와 Assistant에 있는 메소드가 충돌하거나 임의의 메소드가 실행되는 것을 방지할 수 있다.

📌 델리게이션의 주의사항


델리게이션 구현 시 주의사항

Manager는 JavaProgrammer의 인스턴스에게 델리게이션을 요청했다. Manager는 JavaProgrammer를 사용할 수 지만 Manager를 JavaProgrammer로 사용할 수는 없다, 즉, Manager는 JavaProgrammer를 가지고 있는 것이지 한 종류가 아니다. 델리게이션은 상속과 다르게 우연히 대체될 가능성이 없는 재사용성을 제공해 준다.

❗하지만 코틀린의 델리게이션 구현에는 부작용이 있다.

델리게이션을 사용하는 클래스는 위임한 인터페이스를 구현해야 한다. 델리게이션을 사용하는 클래스의 참조가 위임할 인터페이스가 필요한 메소드에 전달될 수 있다.

❌ Error

val corder: JavaProgrammer = doe //Error : Type mismatch

⭕ Possible

 val employee:Worker = doe

이게 의미하는 바는 Manager가 Worker의 한 종류라는 뜻이다. 델리게이션의 진짜 목적은 Manager가 Worker를 이용하는 것이다. 하지만 코틀린의 델리게이션 구현의 부작용으로 Manager는 Worker로 취급된다.

속성을 델리게이션으로 이용할 때의 주의사항

델리게이션 속성 선언을 val 에서 var어 변경하면 몇 가지 결과가 발생한다.

val->var


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 CShareProgrammer : 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}")
doe.work()
println("changing staff")
doe.staff = CShareProgrammer()
println("Staff is ${doe.staff.javaClass.simpleName}")
doe.work()

Manager의 주 생성자는 staff이란 이름의 델리게이션 속성을 뮤터블로 정의했다.
JavaProgrammer를 전달해서 doe 인스턴스를 만든 후 CShareProgrammer 클래스로 변경했다. 객체 타입을 확인하고 work()메소드를 호출했다.

class Manager(var staff: Worker) : Worker by staff

델리게이션이 무엇인지 놓치기 쉽다. 가장 오른쪽에 있는 델리게이션은 속성이 아니라 파라미터이다. 선언에서 실제로 staff란 이름의 파라미터를 받은 후 staff란 이름의 멤버에 할당한다.
마치 this.staff = staff 과 동일하다.

주어진 객체에는 두 개의 참조가 있다
1. 클래스 안에 백킹 필드로서 존재하는 참조
2. 델리게이션 목적으로 존재하는 참조

우리가 CShareProgrammer의 인스턴스로 속성을 변경했을 때 우리는 필드만 변경한 것이지 델리게이션의 참조를 변경한 것이 아니다.

💻 출력

Staff is JavaProgrammer
..write java..
changing staff
Staff is CShareProgrammer
..write java..

코틀린은 객체의 속성이 아닌 주 생성자에 보내진 파라미터로 델리게이션을 한다.

📌 변수와 속성 델리게이션


델리게이션은 객체의 속성과 지역변수에 접근하기 위해 설정할 수도 있다.
속성이나 지역변수를 읽을 때, 코틀린 내부에서는 getValue() 함수를 호출한다. 속성이나 변수를 설정할 때 코틀린은 setvalue() 함수를 호출한다.

객체의 델리게이션을 위의 두 메소드와 함께 제공함으로써 우리 객체의 속성과 지역변수를 읽고 쓰는 요청을 가로챌 수 있다.

변수 델리게이션

우리는 지역변수의 읽기와 쓰기에 대한 접근을 모두 가로챌 수 있다. 그리고 리턴되는 것을 변경할 수 있고 데이터를 언제, 어디서 저장하는지도 변경할 수 있다.

💡 stupid 단어를 입력하면 필터링하는 델리게이션을 만들어보자.

PoliteString

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
    }

}

PoliteString 클래스는 델리게이션으로 접근해야만 작동하도록 되어있다. PoliteString 클래스는 content라는 뮤터블한 속성을 받는다. getValue() 함수에서 문제가 되는 단어를 정리하고 문자열을 반환해 준다. 메소드는 operator 표기로 작성되어 "="
기호를 get 또는 set에 사용할 수 있다.

politecomment

var comment: String by PoliteString("Some nice message")
println(comment)
comment = "This is stupid"
println(comment)
println("comment is of length : ${comment.length}")

💻 출력

Some nice message
This is s*****
comment is of length : 14

PoliteString을 임포트하고 comment 변수를 PoliteString을 사용하도록 변경했다.
String이 PoliteString에 접근할 때 델리게이션을 사용한다.

top-level 함수를 이용하면 더 간단히 접근할 수 있다.

PoliteString

fun beingpolite(comment: String) = PoliteString(comment)

politecomment

var beingComment:String by beingpolite("Some nice message")

속성 델리게이션

지역 변수뿐만이 아니라 객체의 속성에도 델리게이션 접근을 할 수 있다. 속성을 정의할 때 값을 할당하는 게 아니라 by를 사용하고 그 뒤에 델리게이션을 위치하면 된다.

politecomment

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
    }
}

String 파라미터를 받는 대신에 MutableMap<String, Any>를 받아서 comment의 값을 저장한다. getValue() 메소드 안에서 속성의 이름을 key로 사용해서 map에 있는 값을 리턴해 준다. 값이 존재한다면 String으로 캐스팅고도 값이 없다면 빈 문자열을 리턴해 준다.

PostComment

class PostComment(dataSource: MutableMap<String, Any>) {
    val title: String by dataSource
    var likes: Int by dataSource
    val comment: String by Politecomment.PoliteString(dataSource)
    override fun toString(): String = "Title : $title Likes : $likes Commen : $comment"
}

주 생성자가 MutableMap<String, Any> 타입의 기존 데이터 dataSource를 파라미터를 받는다. datasource는 이 클래스의 속성을 위임받아서 처리해 주는 델리게이션이다.
PostComment의 인스턴스의 속성인 title을 읽을 때, 코틀린은 델리게이션 dataSource에 속성 이름인 title을 전달하여 dataSource 속성의 getValue() 메소드를 실행한다. 따라서 map은 key인 title의 값이 존재할 경우 값을 리턴해 준다.
comment 속성의 읽기와 쓰기는 PostString 델리게이션의 getValue() 와 setValue()을 호출한다.


val data: List<MutableMap<String, Any>> = 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)
println(forPost2)

💻 출력

Title : Using Delegation Likes : 3 Commen : Keep it Simple, s*****
Title : Using Inheritance Likes : 1 Commen : Prefer Delegation where possible

객체 하나가 모든 속성을 위임할 필요는 없다. 여기에서 본 것처럼 객체는 속성을 다른 델리게이션들에게 위임할 수 있고, 내부적으로 필드를 저장하고 있을 수도 있다.

📌 빌트인 스탠다드 델리게이션


코틀린은 우리가 실제로 유용하게 사용할 만한 몇 가지 빌트인 델리게이션을 제공해 준다.

✔ observable
속성의 값이 면하는 것을 지켜보게 해주는 유용한 기능
✔ Vetoable
기본 규칙이나 비즈니스 로직에 기반한 속성이 변경되는 것을 막아준다.

조금 게을러도 괜찮다

대부분의 프로그래밍 언어는 단축 평가를 지원한다. 단축 평가란 지금까지 진행한 식의 평가가 결과를 도출하기에 충분할 경우 식의 실행을 건너뛰는 것을 말한다.
지연 델리게이션은 이런 접근의 영역을 넓혀준다.

fun getTemperature(city: String): Double {
  println("fetch from webserviece for $city")
  return 30.0
}

val showTemperature = false
val city = "Boulder"
if (showTemperature && getTemperature(city) > 20)
  println("Warm")
else
  println("Nothing to report")

showTemperature 의 결과는 false 이다. 단출 평가식 덕분에 getTemperature() 메소드는 생략된다. 작업의 결과가 사용되지 않으면 해달 작업을 하지않아 아주 효율적이다.

fun getTemperature(city: String): Double {
  println("fetch from webserviece for $city")
  return 30.0
}

val showTemperature = false
val city = "Boulder"
val temperature = getTemperature(city)
if (showTemperature && temperature > 20)
  println("Warm")
else
  println("Nothig to report")

하지만 getTemperature() 의 결과를 지역 임시변수에 저장하면 효율성이 떨어져 버린다.
단축평가 때문에 temperature 변수는 사용되지도 않았다.

💡지연 델리게이션을 이용하면 이러한 문제가 해결된다.

lazy


fun getTemperature(city: String): Double {
    println("fetch from webserviece for $city")
    return 30.0
}

val showTemperature = false
val city = "Boulder"
val temperature by lazy { getTemperature(city) }
if (showTemperature && temperature > 20)
    println("Warm")
else
    println("Nothig to report")

변수 temperature를 by 키워드를 사용해서 델리게이션 속성으로 변경했다. lazy 함수는 연산을 실행 할 수 있는 람다 표현식을 아규먼트로 받는다. 람다 표현식의 연산은 변수의 값이 필요할 때만 수행된다.
temperature가 정의되는 시점에서 실행되는 게 아니라 Boolean 값 평가 showTemperature 이후에 temperature > 20 평가가 필요한 시점에서 실행이 된다.

옵저버블 델리게이션

observable()은 연관된 변수나 속성의 변화를 가로채는 ReadWriteProperty 델리게이션을 만든다. 변화가 발생하면 델리게이션이 개발자가 observable() 함수에 등록한 이벤트 핸들러를 호출한다.

observable

var count by Delegates.observable(0) { property, oldValue, newValue ->
    println("Property : $property old : $oldValue new : $newValue")
}
println("The value pf count is : $count")
count++
println("The value of count is $count")
count--
println("The value of count is : $count")

변수 count의 값을 변화시키면 이벤트 핸들러가 호출된다.

💻 출력

The value pf count is : 0
Property : var chapter9.delegation.Observe.count: kotlin.Int old : 0 new : 1
The value of count is 1
Property : var chapter9.delegation.Observe.count: kotlin.Int old : 1 new : 0
The value of count is : 0

거부권을 연습하자

리턴 타입이 Unit인 observable로 핸들러를 등록하는 것과는 다르게 vetoable 핸들러를 등록하면 Boolean 결과를 리턴받을 수 있다.

vetoable

var count by Delegates.vetoable(0) { _, oldValue, newValue -> newValue > oldValue }

println("The value pf count is : $count")
count++
println("The value of count is $count")
count--
println("The value of count is : $count")

새로운 값이 이전 값보다 클 경우 람다 표현식이 true를 리턴하고 새로운 값이 이전 값보다 작거나 같을 경우 false를 리턴한다. count가 증가하는 경우에만 허용된다는 뜻이다.

💻 출력

The value pf count is : 0
The value of count is 1
The value of count is : 1
  




🔑 정리


코틀린은 델리게이션을 이용해서 객체의 호출 기능과, 지역변수와 속성 모두에 액세스하는 기능을 제공한다.

by 키워드를 사용하면 getValue() 메소드와 setValue() 메소드를 구현한 모든 객체에 델리게이션을 사용할 수 있다.

개발자는 몇가지 코틀린 스탠다드 라이브러리의 빌트인 스탠다드 델리게이션도 사용할 수 있다.(지연 델리게이션, observable, vetoable)



출처 : 다재다능 코틀린 프로그래밍

profile
개발하고싶은사람

0개의 댓글