educative - kotlin - 9

Sung Jun Jin·2021년 3월 28일
0

Extension through Delegation

Delegation (클래스 위임) 이란?

코틀린에서는 모든 클래스가 자바와 달리 final이다. 클래스 상속이 필요하면 상속될 클래스에 open 접근자를 명시해줘야 한다. 그래야 open이 붙은 클래스를 수정할 때 하위 클래스가 있다는 사실을 상기하여 수정할 수 있다. 그래서 상속 기능이 필요할 때 데코레이터 페턴(Decorator Pattern)이라는 방식을 사용하는데, 상속하고 싶은 클래스와 동일한 인터페이스를 구현하는 새로운 클래스를 만들고 상속하고 싶은 클래스는 내부 프로퍼티로 가지는 방식이다.

상속은 흔하고, 객체지향 언어에서 자주 사용해 파생된 기능이 많다는게 가장 큰 장점이다. 위임은 상속보다 좀 더 유연하지만 아직 객체지향 언어에서 지원하는 특별한 기능이 없다. 코틀린은 상속과 위임 둘 다 지원한다.

Choose wisely

  • 클래스의 인스턴스를 다른 클래스의 인스턴스 대신 사용하려면 상속을 사용해라
  • 클래스의 인스턴스가 다른 클래스의 인스턴스를 사용하게 하려면 위임을 사용해라

무슨 차이인가?

계층구조를 상세화하고 부모, 자식 클래스간의 관계를 표현하고 싶다면 상속을 사용
Animal -> Dog
객체의 다른 인터페이스를 단순히 사용하고 싶다면 위임을 사용
Manager -> Assistant

상속과의 차이점은 위임을 사용하면 인터페이스가 갖는 메소드 이외의 메소드들은 상속받지 않기 때문에 필요한 메소들만 상속 받을 수 있다는 점이다. 하지만 필요한 메소드를 사용하기 위해서는 많은 양의 duplicated code를 작성해야 하는데 코틀린에서는 위엠에서 자바보다 좀 더 편리하고 직관적인 기능을 지원한다.

Designing with Delegates

좀 더 구체적인 예시를 들어보자. Worker 인터페이스를 정의한다. 두 가지 메소드가 있다.

  • 일하기
  • 휴가 써버리기
interface Worker {
    fun work()
    fun takeVacation()
}

위 Worker 인터페이스를 구현한 2개의 클래스(JavaProgrammer, CSharpProgrammer)를 만들어보자

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

class CSharpProgrammer: Worker {
    override work() = println("...write C#...")
    override takeVacation() = println("...branch at the ranch...")
}

회사에는 Worker 인터페이스를 구현한 자바, C# 프로그래머가 존재한다. 그리고 회사에는 프로젝트의 Manager 또한 존재한다.

class Manager

회사는 매니저를 통해 프로젝트를 전달하고 진행시키고 싶어한다. 따라서 매니저는 앞서 구현된 두 개발자들에게 구현된 work() 메소드를 통해 개발자들에게 코드를 뽑아 제품을 만들어야 한다. 하지만 어떻게?

일단 가장 먼저 흔히 사용하는 상속을 통해 구현해보자.

open class JavaProgrammer: Worker {
    // 내용 생략
}

class Manager: JavaProgrammer() // 내용 생략

val pm = Manager()
pm.work() // ...write Java...
  • JavaProgrammer 클래스에 open 접근자를 사용해 상속이 가능한 상태로 만들어준다
  • 그리고 Manager 클래스가 JavaProgrammer를 상속받는다
  • 상속받은 work() 메소드를 사용해 Java 코드를 뽑아낸다....(?)

뭔가 이상하다. 결국 매니저가 자바 코드를 치고 있다. 그건 둘째치고 이렇게 되면 C# 개발자는 놀고있는 셈이 된다. 슬프게도 위와 같은 식으로 코드를 구현하면 결국 매니저는 아래와 같이 자바 개발자가 되어버린다.

val coder: JavaProgrammer = pm // 정상적으로 컴파일

코틀린의 위임(delegation)을 사용해보자. by 키워드를 사용하면 간단하게 클래스 위임을 구현할 수 있다. by 키워드를 사용하면 해당 인터페이스에게 대한 구현을 다른 객체에 위임중이라는 사실을 명시할 수 있다.

class Manager() : Worker by JavaProgrammer()

상속과 다른점은 Manager 클래스가 work() 메소드를 실행시키기 위해 엉뚱하게 JavaProgrammer를 상속받지 않는다는 점이 있다.

val coder: JavaProgrammer = pm // Type mismatch

하지만 이렇게 되면 Manager 클래스는 JavaProgrammer 클래스만 위임받게 되어 Worker 인터페이스를 구현한 다른 객체에는 접근하지 못한다.

매개변수를 통한 위임을 활용하면 조금 더 유연한 구조가 된다

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

val doe = Manager(CSharpProgrammer())
val roe = Manager(JavaProgrammer())

doe.work() //...write C#...
doe.meeting()//organizing meeting with CSharpProgrammer

roe.work() //...write Java...
roe.meeting()//organizing meeting with JavaProgrammer

How to deal with collisions

여태까지의 구조를 다시 한번 정리해보자

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 work() = println("...write C#...")
    override takeVacation() = println("...branch at the ranch...")
}

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

코틀린은 위임하는 클래스에 대해서 충돌을 방지하기 위해 위임받을 메소드를 override 키워드를 사용해 선택한다.

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

Collisions in the two interfaces

인터페이스간 충돌이 일어날 경우 위임을 받는 클래스가 해당 메소드들을 재정의 해줘야 한다.

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

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

class Manager(val staff: Worker, val assistant: Assistant) :
  Worker by staff, Assistant by assistant {      
    
  override fun takeVacation() = println("of course")
  
  // 메소드 충돌 처리
  override fun fileTimeSheet() {
    print("manually forwarding this...")
    assistant.fileTimeSheet()
  }
}

여기서 Manager 클래스는 2개의 클래스(Worker, Assistant)를 위임받는다 이 중에 중복된 이름의 메소드 fileTieSheet()이 있다. 만약 Manager 클래스에서 위처럼filtTimeSheet() 메소드에 대한 처리를 하지 않는다면 두 인터페이스간의 충돌이 일어날 것이다.

실행결과

val doe = Manager(CSharpProgrammer(), DepartmentAssistant())
doe.fileTimeSheet() //manually forwarding this...No escape from that

Deligating Variables and Properties

클래스의 프로퍼티와 getter, setter 또한 위임할 수 있다.

variables

package com.agiledeveloper.delegates

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

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

properties

import kotlin.reflect.KProperty
import kotlin.collections.MutableMap

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 Likes: $likes Comment: $comment"
}

// sample data 저장
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++

//Title: Using Delegation Likes: 3 Comment: Keep it simple, s*****
println(forPost1)
//Title: Using Inheritance Likes: 1 Comment: Prefer Delegation where possible
println(forPost2)

Built-in Standard Delegates

코틀린에서 제공하는 built-in 위임은 다음과 같다

Observable

말 그대로 프로퍼티를 observable 하게 만들어준다. 이것을 이용하면 프로퍼티의 데이터가 변할 때마다 callback을 받을 수 있다.

import kotlin.properties.Delegates.observable

var count by observable(0) { property, oldValue, newValue -> 
  println("Property: $property old: $oldValue: new: $newValue")
}
                                    
println("The value of count is: $count")
count = count + 1 // Need to be changed later
println("The value of count is: $count")
count = count - 1 // Need to be changed later
println("The value of count is: $count")

실행결과

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

vetoable

vetoable은 observable과 거의 유사하지만 리턴 값이 있다는 차이점이 있다.

import kotlin.properties.Delegates.vetoable

var count by vetoable(0) { _, oldValue, newValue -> newValue > oldValue }
                                    
println("The value of count is: $count")
count = count + 1 // Changed later
println("The value of count is: $count")
count = count - 1 // Changed later
println("The value of count is: $count")

실행결과

The value of count is: 0
The value of count is: 1
The value of count is: 1
profile
주니어 개발쟈🤦‍♂️

0개의 댓글