2. 객체지향 프로그래밍

  • 객체지향 패러다임에서 집중 해야 하는 두 가지

    1. 클래스 보다는 어떤 객체가 필요한지 고민하라
    2. 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
  • 도메인(domain): 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
class Screening(private val movie: Movie, private val sequence: Int, private val whenScreened: LocalDateTime) {
    fun getStartTime() = whenScreened
    fun isSequence(sequence: Int) = this.sequence == sequence
    fun getMovieFee() = movie.fee

    fun reserve(customer: Customer, audienceCount: Int) =
        Reservation(customer, this, calculateFee(audienceCount), audienceCount)

    private fun calculateFee(audienceCount: Int) = movie.calculateMovieFee(this).times(audienceCount.toDouble())
}

class Money(private val amount: BigDecimal) {
    companion object {
        val ZERO = wons(0)

        fun wons(amount: Long) = Money(BigDecimal.valueOf(amount))
        fun wons(amount: Double) = Money(BigDecimal.valueOf(amount))
    }

    fun plus(amount: Money) = Money(this.amount.add(amount.amount))
    fun minus(amount: Money) = Money(this.amount.subtract(amount.amount))
    fun times(percent: Double) = Money(this.amount.multiply(BigDecimal.valueOf(percent)))
    fun isLessThan(other: Money) = amount < other.amount
    fun isGreaterThan(other: Money) = amount >= other.amount
}

interface DiscountPolicy {
    fun calculateDiscountAmount(screening: Screening): Money
}

abstract class DefaultDiscountPolicy(vararg conditions: DisCountCondition) : DiscountPolicy {
    private val conditions: MutableList<DisCountCondition> = conditions.toMutableList()

    override fun calculateDiscountAmount(screening: Screening): Money {
        conditions.forEach {
            if (it.isSatisfiedBy(screening)) {
                return getDiscountAmount(screening)
            }
        }
        return Money.ZERO
    }

    abstract fun getDiscountAmount(screening: Screening): Money
}


class AmountDiscountPolicy(
    private val discountMoney: Money,
    vararg conditions: DisCountCondition
) : DefaultDiscountPolicy() {
    override fun getDiscountAmount(screening: Screening) = discountMoney
}

class PercentDiscountPolicy(
    private val percent: Double,
    vararg conditions: DisCountCondition
) : DefaultDiscountPolicy() {
    override fun getDiscountAmount(screening: Screening) = screening.getMovieFee().times(percent)
}

interface DisCountCondition {
    fun isSatisfiedBy(screening: Screening): Boolean
}

class SequenceCondition(private val sequence: Int) : DisCountCondition {
    override fun isSatisfiedBy(screening: Screening) = screening.isSequence(sequence)
}

class PeriodCondition(
    private val dayOfWeek: DayOfWeek,
    private val startTime: LocalTime,
    private val endTime: LocalTime
) : DisCountCondition {
    override fun isSatisfiedBy(screening: Screening) =
        screening.getStartTime().dayOfWeek.equals(dayOfWeek) && startTime <= screening.getStartTime()
            .toLocalTime() && endTime >= screening.getStartTime().toLocalTime()
}

class NoneDisCountPolicy() : DiscountPolicy {
    override fun calculateDiscountAmount(screening: Screening) = Money.ZERO
}

data class Movie(
    val title: String,
    val runningTime: Duration,
    val fee: Money,
    val discountPolicy: DiscountPolicy
) {
    fun calculateMovieFee(screening: Screening) = fee.minus(discountPolicy.calculateDiscountAmount(screening))
    fun changeDiscountPolicy(discountPolicy: DiscountPolicy) = this.copy(discountPolicy = discountPolicy)
}

data class Reservation(
    val customer: Customer, val screening: Screening, val fee: Money, val audienceCount: Int
)

class Customer()

/////////////////////////
fun main() {
    var avatar = Movie("아바타", 210.minutes, Money.wons(10000), NoneDisCountPolicy())
    println(avatar.discountPolicy)

    avatar = avatar.changeDiscountPolicy(PercentDiscountPolicy(0.1, SequenceCondition(1), SequenceCondition(6)))
    println(avatar.discountPolicy)
}

자율적인 객체

  1. 객체는 상태(State)행동(Behavior) 을 함께 가진 복합적인 존재
  2. 객체는 스스로 판단하고 행동하는 자율적인 존재
  • 캡슐화: 데이터기능을 객체 내부로 함께 묶는 것

  • OOP 언어들은 접근 수정자를 통해, 외부에서의 접근을 통제할 수 있는 접근 제어 매커니즘 을 제공

  • 캡슐화와 접근 제어는 객체를 두 부분으로 나눈다.

    1. 퍼블릭 인터페이스: 외부에서 접근가능한 부분
    2. 구현(implementation): 외부에서 접근 불가능하고 오직 내부에서만 접근 가능한 부분
  • 설계가 필요한 이유는 변경을 관리하기 위함이다.

    • OOP는 의존성을 적절히 관리해서, 변경에 대한 파급효과를 제어할 수 있는 다양한 방법 제공, 대표적으로 접근 제어

협력하는 객체들의 공동체

  • OOP의 장점은 객체를 이용해 도메인의 의미를 풍부하게 표현 할 수 있다는 것
    • 의미를 좀 더 명시적이고 분명하게 표현할 수 있다면 객체를 사용해라
    • 그 개념이 비록 하나의 인스턴스 변수만 포함하더라도 개념을 명시적으로 표현하는 것은 설계의 명확성과 유연성을 높히는 첫 걸음이다.

협력에 관한 짧은 이야기

  • 객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 request(요청) 한다.
  • 요청 받은 객체는 자율적인 방법에 따라 요청을 처리하고 response(응답) 한다.
  • 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송(send) 하는 것뿐이다.
  • 다른 객체에게 요청이 도달할 때 해당 객체가 메시지를 수신(receive) 했다고 이야기 한다.
  • 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드(method) 라고 한다.
  • 메시지와 메서드의 구분에서부터 다형성(polymorphism) 개념이 시작한다.

컴파일 시간 의존성과 런타임 의존성

  • 코드 의존성(컴파일 의존성)과 런타임 의존성이 서로 다를 수 있다.

    • 클래스 사이의 의존성과 객체 사이의 의존성은 다를 수 있다.
    • 두 개가 다를수록 코드를 이해하기 어려워진다.
    • 두 개가 다를수록 코드는 더 유연해지고 확장 가능하다
    • 설계는 트레이드오프의 산물이다.
  • 설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다. (트레이드 오프)

    • 반대로, 유연성을 억제하면, 재사용성과 확장 가능성은 낮아진다.
    • 훌륭한 객체지향 설계자는 항상 유연성과 가독성이에서 고민해야 한다.

차이에 의한 프로그래밍

  • 차이에 의한 프로그래밍(progamming by difference): 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법

상속과 인터페이스

  • 상속이 가치 있는 이유는 재사용도 있지만, 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.
  • 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다.
  • 외부 객체는 자식 크래스를 부모 클래스와 동일한 타입으로 간주 할 수 있다.
  • 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcasting) 이라 한다.

다형성

  • 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라지는 것 다형성이라 한다.

  • 다형성은 컴파일 시간 의존성과 런타임 시간 의존성이 다를 수 있다는 사실에 기반으로 한다.

  • 다형성이란 동일한 메시지를 수신했을 때 객체에 타입에 따라 다르게 응답할 수 있는 능력

    • 다형적인 협력에 참여할 수 있는 이유는 객체가 동일한 인터페이스를 물려 받았기 때문
  • 다형성을 구현하는 방법은 매우 다양하지만, 메시지에 대한 실행 메서드를 컴파일이 아니라 런타임에 결정한다는 공통점이 있다.

    • 지연(lazy) 바인딩, 동적(dynamic) 바인딩 이라 한다.
    • 컴파일에 결정하는 것을 초기(early) 바인딩, 정적(static) 바인딩 이라한다.
    • 다형성은 추상적인 개념이며, 구현 할 수 있는 방법이 상속 말고도 다양하다.
  • 구현 상속: 서브클래싱(subclassing)

    • 순수하게 코드를 재사용하기 위해서 상속하는 것
    • 변경에 취약할 수 있다
  • 인터페이스 상속: 서브타이핑(subtyping)

    • 다형적인 협력을 위해 부모, 자식이 인터페이스를 공유할 수 있도록 상속하는 것
    • 상속은 인터페이스 상속을 위해 사용해야 한다

추상화의 힘

  • 추상화의 장점

    1. 요구사항의 정책을 높은 수준에서 서술할 수 있다. (협력 흐름을 기술한다.)
      • 세부적인 내용을 무시한 채 상위 정채을 쉽고 간단하게 표현 가능
      • 표현의 수준을 조정하는 것을 가능하게 한다.
      • Ex) “영화 예매 요금은 최대 하나의 ‘할인 정책’과 다수의 ‘할인 조건’을 이용해 계산할 수 있다.”
    2. 설계가 좀 더 유연해진다.
      • 설계가 구체적인 상황(가령, 예외 케이스)에 결합되는 것을 방지하기 때문
  • 디자인 패턴이나 프레임워크 모두 추상화를 이용해 상위정책을 정의하는 객체지향 메커니즘을 활용한다.

유연한 설계

  • 항상 예외 케이스를 최소화하고 일관성을 유지할 수 있는 방법을 선택해야 한다.

    • 일관성 있는 협력 방식이 무너지게 하지 않게 해라
  • 결론: 유연성이 필요한 곳에 추상화를 사용하라.

코드 재사용, 상속, 합성

  • 코드 재사용을 위해서는 상속보다 합성(composition) 이 더 좋은 방법

    • MovieDiscountPolicy의 인터페이스를 통해 코드를 재사용하는 방법이 바로 합성
    • 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라 한다.
  • 상속은 캡슐화를 위반하고, 설계를 유연하지 못하기에 설계에 안 좋은 영향을 미친다.

    • 상속하기 위해서는 부모 클래스 구조를 잘 알고 있어야 한다.
    • 부모의 내부 구현이 자식에게 노출된다. 캡슐화가 약해짐
  • 상속은 부모, 자식 관계를 컴파일 시점에 결정 => 설계가 유연해지 못하게 됨

  • 상속보다 인스턴스 변수로 관계를 연결한 설계가 더 유연하다.

    • changeDiscountPolicy 함수 추가

Reference