아이템 36
상속보다는 컴포지션을 사용하라
간단한 행위 재사용
상속을 사용하면 공통되는 행위를 추출해 중복된 코드를 제거하고, 하나의 코드로 재사용 가능하다.
행위 재사용의 단점
- 상속은 하나의 클래스만을 대상으로 할 수 있다. 공통 기능을 계속 추출하다 보면 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)
}
}
위와같이 코드를 작성하면 기존 슈퍼클래스가 서브클래스의 캡슐화를 깨뜨리는 상황은 발생하지 않는다.
'Reading Record > 이펙티브 코틀린' 카테고리의 다른 글
[이펙티브 코틀린] Item 38. 연산 또는 액션을 전달할 때는 인터페이스 대신 함수 타입을 사용하라 (0) | 2022.06.07 |
---|---|
[이펙티브 코틀린] Item 37. 데이터 집합 표현에 data 한정자를 사용하라 (0) | 2022.06.07 |
[이펙티브 코틀린] Item 35. 복잡한 객체를 생성하기 위한 DSL을 정의하라 (0) | 2022.06.07 |
[이펙티브 코틀린] Item 34. 기본 생성자에 이름 있는 옵션 아규먼트를 사용하라 (0) | 2022.06.07 |
[이펙티브 코틀린] Item 33. 생성자 대신 팩토리 함수를 사용하라 (0) | 2022.06.07 |