ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotlin by keyword를 이용해 상속대신 Delegation을 해보자
    프로그래밍/Kotlin 2019. 9. 3. 14:02
    반응형

    이 문서는 아래 문서인 [Kotlin “By” Class Delegation: Favor Composition Over Inheritance] 을 번역, 보강한 문서이다. 

    https://medium.com/rocket-fuel/kotlin-by-class-delegation-favor-composition-over-inheritance-a1b97fecd839

     

    Kotlin “By” Class Delegation: Favor Composition Over Inheritance

    When people ask me why I choose Kotlin over Java, I often say, “Because Kotlin is a better Java.” You get more than half of Effective Java implemented for you. In chapter 4 of the book, the author…

    medium.com

    상속이란 부모 클래스의 프로퍼티와 메소드를 모두 사용하는 개념이나 Kotiln은 상속을 별로 좋아하지 않는다. Kotlin Class 기본적으로 final 구현된다. 상속을 피하기 위한 방법 중 하나로 Delegation 방법이 있는데 클래스 상속하지 않고 instance를 다른 클래스에 전달해 사용하는 것이다. 이때 Kotlin은 언어 단에서  by라는 keyword로 Delegation을 제공한다. 

     

    문제1. 상속 문제점

    Set 하나 상속해보자. Set 아이템이 add되는 갯수를 count하는 기능을 추가해보자

    사용법은 아주 간단한데 HashSet 상속하고 add()/addAll() 호출될때마다 addedCount 증가하게된다.

    public class CountingSet extends HashSet<Long> {
    
        private long addedCount = 0;
    
        @Override
        public boolean add(Long aLong) {
            addedCount++;
            return super.add(aLong);
        }
    
        @Override
        public boolean addAll(Collection<? extends Long> c) {
            addedCount = addedCount + c.size();
            return super.addAll(c);
        }
    
        public long getAddedCount() {
            return addedCount;
        }
    }

    하지만 결과를 보면 원하는데로 나오지 않는다

    val countingSet = CountingSet()
    countingSet.addAll(setOf(1, 3, 4))
    println("added count is ${countingSet.addedCount}") // prints: added count is 6
    

    문제는 HashSet#addAll() 있다. 사실 addAll() 내부적으로 add() 호출하게 되어있다. 그래서 addedCount 두번씩 증가하게 되는 것이다

    public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }

    만약에 HashSet 내부구현을 알고 있었으면 addedCount add() 안에만 추가하면 된다. 하지만 이 문제만 해결된다고 안심할 수 없다. HashSet 직접 Override하고 있기 때문에 HashSet 내부가 변하지 않을 것이라는 보장은 없다

     

    문제2. Constructor 초기화 순서

    constructor println()하는 부분을 추가했다

    abstract class Animal {
    
        init {
            println("I'm ${getName()}")
        }
    
        abstract fun getName(): String
    }
    
    class Dog(private val name: String) : Animal() {
    
        override fun getName(): String = name
    }
    
    fun main(args: Array<String>) {
        val dog = Dog("Puff") // prints: I'm null
    }
    

    하지만 역시 원하는대로 출력되지 않는다. getName() null return하는 이유는 Java super()에서는 constructor 먼저 호출해야하기 때문이다. Dog name 아직 assign되지 않고 호출되게 된다.

    이 문제는 일반적인 오류로 IDE에서 경고해주고 있다.

     

    위의 문제를 피하기 위해, 상속과 인터페이스 구현을 최대한 피하고 잠재적 이슈를 줄여야한다. 

     

    Delegation in Java

    이미 많은 라이브러리와 프레임워크에서 사용되고 있는 Java에서 Delegation 보여준다.

    Google core library Guava에서는 Forwarding collections set 구현하고 있는데, 이것은 extending하지 않고 collection interface 사용할 있도록 하고 있다

     

    CountingSet v2

    ForwardingSet 사용해서 만든 addedCount 값을 더하는 클래스이다. (ForwardingCollection), (ForwadingObject)

    public class CountingSet extends ForwardingSet<Long> {
    
        private final Set<Long> delegate = Sets.newHashSet();
        private long addedCount = 0L;
    
        @Override
        protected Set<Long> delegate() {
            return delegate;
        }
    
        @Override
        public boolean add(Long element) {
            addedCount++;
            return delegate().add(element);
        }
    
        @Override
        public boolean addAll(Collection<? extends Long> collection) {
            addedCount += collection.size();
            return delegate().addAll(collection);
        }
    }
    

    이전 것과 비슷해보이지만 Set add() addAll() 의존하고 있지 않다. `forward`라는 말은 method call delegate한다는 의미이기 때문이다. 우리는 delegate detail 알필요 없이 다른 타입과 대체할 있다.

     

    하지만 클래스는 여전히 abstract이긴 하다. Java `delegation` Kotlin처럼 언어단에서 support하진 않는다

    다른 deletation 예로 많이 사용하는 AppCompatActivity 있다

    /**
     * @return The {@link AppCompatDelegate} being used by this Activity.
     */
    @NonNull
    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }
    
    // inside AppCompatDelegate.java
    private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        if (Build.VERSION.SDK_INT >= 24) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV9(context, window, callback);
        }
    }

    같은 패턴으로 delegates 직접 생성해서 AppCompatDelegate 다른 구현들을 사용하고 있다

     

    Delegation in Kotlin

    다음 예제 코드는 Kotlin `by` 키워드로 아주 간단해질 것이다. 우리는 deletation 줄에 사용할 있다

     

    CountingSet v3

    Kotlin으로 같은 CountingSet 구현해보자.

    CountingSet.kt

    class CountingSet(private val delegate: MutableSet<Long> = HashSet()) : MutableSet<Long> by delegate {
    
        private var addCount = 0L
    
        override fun add(element: Long): Boolean {
            addCount++
            return delegate.add(element)
        }
    
        override fun addAll(elements: Collection<Long>): Boolean {
            addCount += elements.size
            return delegate.addAll(elements)
        }
    }
    

    CountingSet.decompiled.kt

    public final class CountingSet implements Set, KMutableSet {
       private long addCount;
       private final Set delegate;
    
       public boolean add(long element) {
          int var10001 = this.addCount++;
          return this.delegate.add(element);
       }
       public boolean addAll(@NotNull Collection elements) {
          Intrinsics.checkParameterIsNotNull(elements, "elements");
          this.addCount += (long)elements.size();
          return this.delegate.addAll(elements);
       }
       public int getSize() {
          return this.delegate.size();
       }
       public boolean isEmpty() {
          return this.delegate.isEmpty();
       }
    }

     

    우리가 알고 있는 몇가지를 토대로 살펴보자.

    #1 “class CountingSet(private val delegate: MutableSet<Long> = HashSet()) : MutableSet<Long> by delegate”에서 ": MutableSet" 에 들어가는 Deletation 타입은 interface여야한다. abstract class 안된다. (IDE에서도 막고있음)

    #2 Deletation primary constructor property거나 interface Type이어야한다위의 예제에서는 add() addAll() override한다. 

    #3 아래 코드처럼 `by` 에 구현체를 직접 넘겨주어 Constructor property 사용하지 않고 작성해도 된다.

    CountingSet.kt

    CountingSet.kt
    
    class CountingSet() : MutableSet<Long> by HashSet() {
    }

     

    CountingSet.decompiled.java

    public final class CountingSet implements Set, KMutableSet {
    
      private final HashSet $$delegate_0 = new HashSet();
    
      public boolean add(long element) {
          return this.$$delegate_0.add(element);
       }
    
      public boolean addAll(@NotNull Collection elements) {
          Intrinsics.checkParameterIsNotNull(elements, "elements");
          return this.$$delegate_0.addAll(elements);
       }
    }

     

    #4 아래 코드처럼 Deletation 하나 이상 사용해도 된다. Set<Long> by HashSet()  Map<Long, Long> by HashMap() 개의 Delegator 생성해보자.

    MySetMap.kt

    class MySetMap : Set<Long> by HashSet(), Map<Long, Long> by HashMap() {
        override val size: Int
            get() = TODO("not implemented")
    
        override fun isEmpty(): Boolean {
            TODO("not implemented")
        }
    }

    Map, Set size isEmepty() 구현해야한다


    MySetMap.decompiled.java

    public final class MySetMap implements Set, Map, KMappedMarker {
       // $FF: synthetic field
       private final HashSet $$delegate_0 = new HashSet();
       // $FF: synthetic field
       private final HashMap $$delegate_1 = new HashMap();
       public boolean add(long var1) {
          throw new UnsupportedOperationException("Operation is not supported for read-only collection");
       }
       public boolean addAll(Collection var1) {
          throw new UnsupportedOperationException("Operation is not supported for read-only collection");
       }
       ...
    }

    하지만 delegate를 직접 참조할 수 없기 때문에 구현해야만하는 메소드에 대한 수정이 어렵다. by 는 하나만 사용하는 것이 좋겠다. (변역자의견) 

    앞으로 `by` 키워드를 이용해서 interface를 직접 상속하지 않고 메소드 delegation을 이용해보고 싶을 때 사용할 수 있을 것 같다. 

     

    refer - https://speakerdeck.com/pluu/kotlineul-yeohaenghaneun-hicihaikeoyi-junbiseo

    반응형
Designed by Tistory.