상속보다는 컴포지션을 사용하라

상속보다는 컴포지션을 사용하라

간단한 행위 재사용

상속을 사용하면 공통되는 행위를 추출해 중복된 코드를 제거하고, 하나의 코드로 재사용 가능하다.

행위 재사용의 단점

상속은 하나의 클래스만을 대상으로 할 수 있다. 공통 기능을 계속 추출하다 보면 BaseXXX 같은 거대한 클래스가 탄생할 수 있다.

상속은 클래스의 모든것을 가져오게 된다. 불필요한 함수를 갖는 클래스가 만들어 질 수 있다. (인터페이스 분리 원칙 위배)

상속은 이해하기 어렵다. 동작이해를 위해 슈퍼클래스를 왔다 갔다하며 코드를 봐야 한다.

컴포지션을 활용하면 위 단점이 해결된다.

class Progress{
    fun showProgress(){}
    fun hideProgress(){}
}

class ProfileLoader{
    val progress = Progress()

    fun load() {
        progress.showProgress()
        //impl
        progress.hideProgress()
    }
}

하나 이상의 클래스를 상속할 수 없다. 위 코드에서 추가 Alert기능이 필요하다면 상속의 경우 두 기능을 하나의 슈퍼 클래스에 배치해야한다. 컴포지션의 경우 멤버변수로 Alert클래스를 추가하면 된다.

모든 것을 가져올 수 밖에 없는 상속

상속은 계층 구조를 나타낼 때 굉장히 좋은 도구이다. 재사용하기 위한 목적으로는 적합하지 않다.

슈퍼클래스의 두 함수가 존재하고 특정 서브클래스에서는 한가지의 함수만 필요한 경우에도 두 함수 모두 가져올 수 있다. 위 상황은 인터페이스 분리 원칙에 위반된다.

캡슐화를 깨는 상속

class CounterSet<T> : HashSet<T>() {
    var added: Int = 0
        private set

    override fun add(el: T): Boolean {
        added++
        return super.add(el)
    }

    override fun addAll(els: Collection<T>): Boolean {
        added += els.size
        return super.addAll(els)
    }
}

    val list = CounterSet<String>()
    list.addAll(listOf("A","B","C"))
    print(list.added) //6

위 코드는 예상과 다르게 동작한다. 슈퍼클래스에서 addAll이 내부적으로 add를 호출해 added값이 2배 증가한다. 이처럼 상속은 캡슐화를 깨기도 한다.

이러한 오류의 해결책은 포워딩 메서드를 작성하는것으로 컴포지션으로 HashSet을 포함시켜 캡슐화를 유지한다.

class CounterSet2<T>(
    private val set: MutableSet<T> = mutableSetOf()
) : MutableSet<T> by set {
    var added: Int = 0
        private set

    override fun add(el: T): Boolean {
        added++
        return set.add(el)
    }

    override fun addAll(els: Collection<T>): Boolean {
        added += els.size
        return set.addAll(els)
    }
}

위와같이 코드를 작성하면 기존 슈퍼클래스가 서브클래스의 캡슐화를 깨뜨리는 상황은 발생하지 않는다.

Last updated