- 컴포지션:
- 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 메서드를 호출하는 기법
- 객체 지향 프로그래밍에서 클래스 간의 관계를 정의하는 방법 중 하나로, 객체가 다른 객체를 포함하여 기능을 구현하는 방식입니다. 컴포지션은 "has-a" 관계를 나타내며, 한 객체가 다른 객체의 구성 요소로 포함되는 구조를 의미합니다.
- 컴포지션은 더 안전하고, 유연하고 명시적이다.
- (컴포지션이란? 다른객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 메서드를 호출하는 기법이다)
- 상속을 사용할때?
- 명확한 is-a 관계일때
- 슈퍼클래스를 상속받은 모든 서브클래스는 슈퍼클래스로도 동작할 수 있어야한다.
- 슈퍼클래스의 단위테스트는 서브클래스로도 통과할 수 있어야 한다는 의미(LSP)
- 오버라이딩 제한
- 어떤 이유로 상속은 허용하지만, 메서드는 오버라이드하지 못하게 만들고 싶은 경우에는 open 키워드를 사용하자
- 서브클래스에서 오버라이드할 수 있는 메서드를 제한하고 싶으면 final 키워드를 사용하자.
- 컴포지션은 다른 클래스의 내부 구현에 의존하지 않기 때문에 더 안전합니다.
- 컴포지션은 여러 클래스를 대상으로 할 수 있기 때문에 더 유연합니다.
- 컴포지션은 this 리시버를 사용할 수 없기 때문에 리시버를 명시적으로 활용해야 해야 더 명시적입니다.
아래는 상속과 컴포지션 을 활용한 코드
상속을 이용한다면
open class Animal {
fun eat() {
println("Eating")
}
}
class Dog : Animal() {
fun bark() {
println("Barking")
}
}
class Bird : Animal() {
fun fly() {
println("Flying")
}
}
fun main() {
val dog = Dog()
dog.eat()
dog.bark()
val bird = Bird()
bird.eat()
bird.fly()
}
===================================================================
컴포지션을 이요한다면
interface EatBehavior {
fun eat()
}
interface BarkBehavior {
fun bark()
}
interface FlyBehavior {
fun fly()
}
class Eating : EatBehavior {
override fun eat() {
println("Eating")
}
}
class Barking : BarkBehavior {
override fun bark() {
println("Barking")
}
}
class Flying : FlyBehavior {
override fun fly() {
println("Flying")
}
}
class Dog(
private val eatBehavior: EatBehavior,
private val barkBehavior: BarkBehavior
) {
fun eat() {
eatBehavior.eat()
}
fun bark() {
barkBehavior.bark()
}
}
class Bird(
private val eatBehavior: EatBehavior,
private val flyBehavior: FlyBehavior
) {
fun eat() {
eatBehavior.eat()
}
fun fly() {
flyBehavior.fly()
}
}
fun main() {
val eating = Eating()
val barking = Barking()
val flying = Flying()
val dog = Dog(eating, barking)
dog.eat()
dog.bark()
val bird = Bird(eating, flying)
bird.eat()
bird.fly()
}
- 추가 코드를 적절하게 처리하는 것이 조금 어려울 수도 있어 컴포지션보다 상속을 선호나는 경우도 많습니다.
하지만 이런 추가 코드로 인해서 코드를 읽는 사람들이 코드의 실행을 더 명확하게 예츨할 수 있다는 장점도 있고, 코드를 훨씬 자유롭게 사용할 수 있다는 장점도 있다.
하나의 클래스 내부에서 여러 기능을 재사용할 수 있게 된다. - 상속은 모든것을 가져올 수밖에 없어서 일부분을 재사용하기 위한 목적으로는 적합하지 않다.
- 244 페이지 강아지 class 상 속받기코드
- ISP, LSP 모두 위배
// p245 이러한 코드는 RobotDog 가 필요도 없는 메서드를 갖기 때문에, ISP 원칙에 위배된다.
// 또한 슈퍼클래스의 동작을 서브클래스에서 깨버리므로 LSP 원칙에도 위반
abstract class Dog {
abstract fun bark()
abstract fun sniff()
}
class RealDog : Dog() {
override fun bark() {
println("Woof!")
}
override fun sniff() {
println("Sniffing...")
}
}
class RobotDog : Dog() {
override fun bark() {
println("Beep boop!")
}
override fun sniff() {
throw UnsupportedOperationException("Robot dogs can't sniff")
}
}
- 해당코드를 컴포지션을 이용한다면? 그리고 로봇강아지는 계산이라는 함수를 추가해야한다면?
// 기능 정의
class Barking {
fun bark() {
println("Woof!")
}
}
class RoboticBarking {
fun bark() {
println("Beep boop!")
}
}
class Sniffing {
fun sniff() {
println("Sniffing...")
}
}
class Calculating {
fun calculate() {
println("Calculating...")
}
}
// 강아지 클래스
class Dog(
private val barking: Barking,
private val sniffing: Sniffing? = null
) {
fun bark() {
barking.bark()
}
fun sniff() {
sniffing?.sniff() ?: println("This dog can't sniff")
}
}
// 로봇 강아지 클래스
class RobotDog(
private val barking: RoboticBarking,
private val calculating: Calculating
) {
fun bark() {
barking.bark()
}
fun calculate() {
calculating.calculate()
}
}
// 사용
fun main() {
val realDog = Dog(Barking(), Sniffing())
realDog.bark()
realDog.sniff()
val robotDog = RobotDog(RoboticBarking(), Calculating())
robotDog.bark()
robotDog.calculate()
}
캡슐화를 깨는 상속
아래코드는 이펙티브 자바에 나오는 문제있는 코드
class CounterSet<T> : HashSet<T>() {
var elementAdded: Int = 0
private set
override fun add(element: T): Boolean {
elementAdded++
return super.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
elementAdded += elements.size
return super.addAll(elements)
}
}
@Test
fun `246p_test`() {
val counterList = CounterSet<String>()
counterList.addAll(listOf("A", "B", "C"))
println(counterList.elementAdded) // 6
}
위 코드에서 6이 잘못 나오는 것은 HashSet 의 addAll 내부에서 add 를 사용했기 때문입니다.
override 막기
open class BaseClass {
open fun openMethod() {
println("This method can be overridden")
}
final fun finalMethod() {
println("This method cannot be overridden")
}
}
class DerivedClass : BaseClass() {
override fun openMethod() {
println("Overridden method")
}
// This will cause a compile error
// override fun finalMethod() {
// println("Trying to override final method")
// }
}
fun main() {
val obj = DerivedClass()
obj.openMethod() // Output: Overridden method
obj.finalMethod() // Output: This method cannot be overridden
}
개인의견
- 인터페이스 분리원칙과 컴포지션은 매우 비슷한거 같다..
- 최대한 간결한 인터페이스, 컴포지션을 구성는게 좋은거 같다
- 다형성 필요 여부에따라 둘중에 어떤걸 사용할지 고민해보면좋을거 같다
- 다형성이 필요하고, 여러 클래스가 동일한 행동을 다르게 구현한다면 인터페이스, 그게 아니라면 컴포지션을 사용
- 좀더 유연한 설계를 구성하고 동적으로 행동을 변경해야한다면 컴포지션이 낫다
- 변경이 빈번하다면 인터페이스보다 컴포지션이 낫다
'공부 > 이펙티브코틀린' 카테고리의 다른 글
아이템 38 - 연산 또는 액션을 전달할 때는 인터페이스 대신 함수 타입을 사용하라 (0) | 2024.08.01 |
---|---|
아이템 37 - 데이터 집합 표현에 data 한정자를 사용하라 (1) | 2024.07.14 |
아이템35 - 복잡한 객체를 생성하기 위한 DSL을 정의하라 (0) | 2024.07.11 |
아이템 34 - 기본 생성자에 이름 있는 옵션 아규먼트를 사용하라 (0) | 2024.07.11 |
아이템 33 - 생성자 대신 팩토리 함수를 사용하라 (0) | 2024.07.11 |
댓글