ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Book] 오브젝트 - 02. 객체지향 프로그래밍 (Object Oriented)
    Archive/CS & App 2022. 5. 19. 07:35

    01 영화 예매 시스템

    특정 조건을 만족하는 예매자에게 할인 제공

    - 할인 조건 (discount condition) : 상영 시점에 따른 할인

          - 순서 조건 (sequence condition)

          - 기간 조건 (period condition)

    - 할인 정책 (discount policy) : 특별한 조건에 따른 할인

          - 금액 할인 정책 (amount discount policy)

          - 비율 할인 정책 (percent discount policy)


    02 객체지향 프로그래밍을 향해

    협력, 객체, 클래스

    클래스가 아닌 객체에 초점을 맞출때

    - 어떤 객체들이 필요한지 고민하기

    - 객체는 기능을 구현하기 위해 협력하는 공동체

     

    도메인의 구조를 따르는 프로그램 구조

    도메인 (Domain) : 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야

    - 도메인 개념들을 구현하기 위해 클래스를 사용한다는 사실

    애플에서 제공하는 대부분의 프레임워크는 객체지향과 프로토콜 지향 구조

     

    클래스 구현하기

    공개하는 것과 감추는 것에 구분을 결정하는게 설계 시 중요함

    // 상영 정보 - 영화 정보나 순번, 시작 등 정보는 숨겨질 필요가 없다고 생각함.
    struct Screening {
        let movie: Movie // 상영할 영화
        let sequence: Int // 순번
        let whenScreened: DateComponents // 상영 시작 시간
        
        var startTime: DateComponents {
            return whenScreened
        }
        
        var movieFee: Money {
            return movie.fee
        }
        
        func isSequence(_ sequence: Int) -> Bool {
            return self.sequence == sequence
        }
        
        func reserve(customer: Customer, audienceCount: Int) -> Reservation {
            return Reservation(customer: customer,
                               screening: self,
                               fee: calculateFee(by: audienceCount),
                               audienceCount: audienceCount)
        }
        
        func calculateFee(by audienceCount: Int) -> Money {
            return movie.calculateMovieFee(self).times(audienceCount)
        }
    }

    자율적인 객체

    - State와 Behavior을 함께 가지는 복합적인 존재, 스스로 판단하고 행동하는 자율적인 존재

    캡슐화 : 데이터와 기능을 객체 내부로 함께 묶는 것

    접근 제어(Access Control) : 외부에서의 접근을 통제할 수 있는 매커니즘

    프로그래머의 자유

    Implementation hiding : 인터페이스 개념, 내부의 구현은 무시한 채 인터페이스만 알고 있어도 클래스를 사용가능

     

    협력하는 객체들의 공동체

    Collaboration : 시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용

    extension Screening {   
        func reserve(customer: Customer, audienceCount: Int) -> Reservation {
            return Reservation(customer: customer,
                               screening: self,
                               fee: calculateFee(by: audienceCount),
                               audienceCount: audienceCount)
        }
        
        func calculateFee(by audienceCount: Int) -> Money {
            return movie.calculateMovieFee(self).times(audienceCount)
        }
    }
    struct Money {
        /* Swift 스타일로 이름을 지어봄
        static let zero: Money = Money.wons(0)
        
        static func wons(_ amount: Int) -> Money {
            return Money(amount: Decimal(amount))
        }
        */
        static func makeZero() -> Money {
            return Money.makeByWon(0)
        }
        
        static func makeByWon(_ amount: Int) -> Money {
            return Money(amount: Decimal(amount))
        }
        
        private var amount: Decimal
        
        func plus(_ amount: Money) -> Money {
            return Money(amount: self.amount + amount.amount)
        }
        
        func minus(_ amount: Money) -> Money {
            return Money(amount: self.amount - amount.amount)
        }
        
        func times(_ percent: Double) -> Money {
            return Money(amount: self.amount * Decimal(percent))
        }
        
        func times(_ count: Int) -> Money {
            return Money(amount: self.amount * Decimal(count))
        }
        
        func isLessThan(other: Money) -> Bool {
            return amount < other.amount
        }
        
        func isGreaterThanOrEqual(other: Money) -> Bool {
            return amount >= other.amount
        }
    }
    // 특별한 기능은 없기 때문에 데이터 인스턴스로 사용, 프로퍼티 공개함
    struct Reservation {
        let customer: Customer
        let screening: Screening
        let fee: Money
        let audienceCount: Int
    }

     

    협력에 관한 짧은 이야기

    message : 개체가 다른 개체와 상호작용할 수 있는 유일한 방법

    method : 수신된 메세지를 처리하기 위한 자신만의 방법

     


    03 할인 요금 구하기

    할인 요금 계산을 위한 협력 시작하기

    어떤 할인 정책을 사용할 것인지 결정하는 코드가 어디에도 존재하지 않음

    > 의존성 주입

    > 영화 정보는 데이터로써 사용될 것 같아서 struct (default)로 적용해 봄 

    // 할인 정책은 메서드로 다루기 때문에 private, 나머지 부분은 공개
    struct Movie { 
        let title: String 				// 제목 
        let runningTime: TimeInterval		// 상영시간
        let fee: Money				// 기본요금
        private var discountPolicy: DiscountPolicy? // 할인 정책
        
        init(title: String, runningTime: TimeInterval, fee: Money, discountPolicy: DiscountPolicy) {
            self.title = title
            self.runningTime = runningTime
            self.fee = fee
            self.discountPolicy = discountPolicy
        }
        
        func calculateMovieFee(_ screening: Screening) -> Money {
            if let discountPolicy = discountPolicy {
                return fee.minus(discountPolicy.calculateDiscountAmount(screening))
            } else {
                return fee
            }
        }
    }

     

    할인 정책과 할인 조건

    TEMPLATE METHOD 패턴 : 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴

    > Swift에는 추상 메서드가 존재 하지 않는다.

    > protocol-extension을 이용한 상속과 protocol을 이용한 구현 위임 형태로 구현해보았다.

    // 추상클래스를 protocol과 extension 구현으로 대체해봄
    protocol DiscountPolicy {
        var conditions: [DiscountCondition] { get set }
    
        func calculateDiscountAmount(_ screening: Screening) -> Money
        func discountAmount(_ screening: Screening) -> Money         // 구현을 채택한 개체에 위임
    }
    
    extension DiscountPolicy {
        func calculateDiscountAmount(_ screening: Screening) -> Money {
            for condition in conditions {
                if condition.isSatisfiedBy(screening) {
                    return discountAmount(screening)
                }
            }
            return Money.makeZero()
        }
    }
    
    extension DiscountPolicy where Self == NoneDiscountPolicy {
        func calculateDiscountAmount(_ screening: Screening) -> Money {
            return Money.makeZero()
        }
    }

    > protocol 로 선언된 프로퍼티는 나의 선택과 달리 우선적으로 internal 인점 아쉬움.. 좀더 고민이 필요함

     

    protocol DiscountCondition {
        func isSatisfiedBy(_ screening: Screening) -> Bool
    }
    struct SequenceCondition: DiscountCondition {
        private let sequence: Int
        
        init(_ sequence: Int) {
            self.sequence = sequence
        }
        
        func isSatisfiedBy(_ screening: Screening) -> Bool {
            return screening.isSequence(sequence)
        }
    }
    extension Date {
        
        func isAvailableTime(from start: Date, to end: Date) -> Bool {
            let startCompared = start.compare(self)
            let canDoFromStart = startCompared == .orderedAscending || startCompared == .orderedSame
            let endCompared = end.compare(self)
            let canDoFromEnd = endCompared == .orderedDescending || endCompared == .orderedSame
            return canDoFromStart && canDoFromEnd
        }
        
        /* Test
        private func DateExtensionTest() {
            let calendar = Calendar.current
            let start = calendar.date(from: DateComponents(hour: 5, minute: 11))!
            let end = calendar.date(from: DateComponents(hour: 5, minute: 13))!
    
            let compareTrue1 = calendar.date(from: DateComponents(hour: 5, minute: 11))!
            let compareTrue2 = calendar.date(from: DateComponents(hour: 5, minute: 12))!
            let compareTrue3 = calendar.date(from: DateComponents(hour: 5, minute: 13))!
    
            let compareFalse1 = calendar.date(from: DateComponents(hour: 5, minute: 10))!
            let compareFalse2 = calendar.date(from: DateComponents(hour: 5, minute: 14))!
    
            print("True  Test 1 \(compareTrue1.isAvailableTime(from: start, to: end))")
            print("True  Test 2 \(compareTrue2.isAvailableTime(from: start, to: end))")
            print("True  Test 3 \(compareTrue3.isAvailableTime(from: start, to: end))")
            print("False Test 1 \(compareFalse1.isAvailableTime(from: start, to: end))")
            print("False Test 2 \(compareFalse2.isAvailableTime(from: start, to: end))")
        }
        */
    }
    
    struct PeriodCondition: DiscountCondition {
        private let dayOfWeek: WeekDay // Calendar.current.component(.weekday, from: Date())
        private let startTime: DateComponents
        private let endTime: DateComponents
        
        init(_ dayOfWeek: WeekDay,_ startTime: DateComponents,_ endTime: DateComponents) {
            self.dayOfWeek = dayOfWeek
            self.startTime = startTime
            self.endTime = endTime
        }
        
        func isSatisfiedBy(_ screening: Screening) -> Bool {
            let calendar = Calendar.current
            
            let whenScreened = screening.startTime
            let whenScreenedDate = calendar.date(from: whenScreened)!
            let whenScreenedWeekday = calendar.component(.weekday, from: whenScreenedDate)
            
            let startTimeDate = calendar.date(from: startTime)!
            let endTimeDate = calendar.date(from: endTime)!
            
            let isSameWeekday = whenScreenedWeekday == dayOfWeek.rawValue
        
            return isSameWeekday && whenScreenedDate.isAvailableTime(from: startTimeDate, to: endTimeDate)
        }
    }

    > 아직 시간다루는 방법이 야생인것 같은데.. 좀 더 고심이 필요할것 같다.

    > 날짜와 시각에 대한 정보를 지정하는 점 때문에 Date가 아니라 DateComponents 타입으로 프로퍼티를 구현함 - Date로 두는게 맞을지 고민중..

    > 강제언래핑 불편...

     

    struct AmountDiscountPolicy: DiscountPolicy {
        var conditions: [DiscountCondition]
        private var discountAmount: Money
        
        init(discountAmount: Money, conditions: DiscountCondition...) {
            self.conditions = conditions
            self.discountAmount = discountAmount
        }
        
        func discountAmount(_ screening: Screening) -> Money {
            return discountAmount
        }
    }
    
    struct PercentDiscountPolicy: DiscountPolicy {
        var conditions: [DiscountCondition]
        private var percent: Double
        
        init(percent: Double, conditions: DiscountCondition...) {
            self.conditions = conditions
            self.percent = percent
        }
        
        func discountAmount(_ screening: Screening) -> Money {
            return screening.movieFee.times(percent)
        }
    }

     

    할인 정책 구성하기

    enum WeekDay: Int {
        case mon = 1
        case tue, wen, thu, fri, sat, sun
    }
    
    let avatarTitle = "아바타"
    let avatarRunningTime = TimeInterval(120*60)
    let avatarFee = Money.makeByWon(10000)
    let avatarDiscountPolicy = AmountDiscountPolicy(discountAmount: Money.makeByWon(800),
                                    conditions: SequenceCondition(1), SequenceCondition(10),
                                            PeriodCondition(.mon,
                                                DateComponents(hour: 10, minute: 0),
                                                DateComponents(hour: 11, minute: 59)),
                                            PeriodCondition(.mon,
                                                DateComponents(hour: 10, minute: 0),
                                                DateComponents(hour: 20, minute: 59)))
    var avatar = Movie(title: avatarTitle,
                       runningTime: avatarRunningTime,
                       fee: avatarFee,
                       discountPolicy: avatarDiscountPolicy)
    
    // 할인 정책 변경예제
    avatar.changeDiscountPolicy(by: PercentDiscountPolicy(percent: 0.1,
                                    conditions: SequenceCondition(1), SequenceCondition(10),
                                            PeriodCondition(.mon,
                                                DateComponents(hour: 10, minute: 0),
                                                DateComponents(hour: 11, minute: 59)),
                                            PeriodCondition(.mon,
                                                DateComponents(hour: 10, minute: 0),
                                                DateComponents(hour: 20, minute: 59))))
    
    let titanicTitle = "아바타"
    let titanicRunningTime = TimeInterval(180*60)
    let titanicFee = Money.makeByWon(11000)
    let titanicDiscountPolicy = PercentDiscountPolicy(percent: 0.1,
                                    conditions: SequenceCondition(2),
                                            PeriodCondition(.mon,
                                                DateComponents(hour: 14, minute: 0),
                                                DateComponents(hour: 16, minute: 59)),
                                            PeriodCondition(.mon,
                                                DateComponents(hour: 10, minute: 0),
                                                DateComponents(hour: 13, minute: 59)))
    let titanic = Movie(title: titanicTitle,
                        runningTime: titanicRunningTime,
                        fee: titanicFee,
                        discountPolicy: titanicDiscountPolicy)
    
    let starwarsTitle = "스타워즈"
    let starwarsRunningTime = TimeInterval(210*60)
    let starwarsFee = Money.makeByWon(10000)
    let starwarsDiscountPolicy = NoneDiscountPolicy()
    let starwars = Movie(title: starwarsTitle,
                         runningTime: starwarsRunningTime,
                         fee: starwarsFee,
                         discountPolicy: starwarsDiscountPolicy)

    04 상속과 다형성

    컴파일 시간 의존성과 실행 시간 의존성

    코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있음

    > Dynamic Dispatch, Static Dispatch 관련

    > Dynamic Dispatch와 성능 최적화 - Rhyno 님의 블로그 참고

    설계가 유연할 수 록 디버깅이 어려움, 반면 유연하지 못하면 재사용성과 확장 가능성 낮아짐 (객체지향 설계의 딜레마)

     

    차이에 의한 프로그래밍 Programming by difference

    부모클래스와 다른 부분만 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법

     

    상속과 인터페이스

    자식 클래스는 부모클래스의 모든 인터페이스를 가지고 있다 (내용은 다를 수 있음)

    업 캐스팅 (upcasting) : 자식 클래스가 부모클래스를 대신하는 것 

     

    다형성

    동일한 메세지를 전송하지만 어떤 메서드가 실행될 지, 메세지 수신 클래스가 무엇인지에 따라 달라짐

    컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반

    lazy, dynamic  binding : 메세지와 메서드를 실행 시점에 바인딩

    early, static binding : 전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정

    구현 상속 (subclassing) : 코드만 재사용 (지양)

    인터페이스 상속 (subtyping) : 다형적 협력을 위해 인터페이스 공유 (상속의 목적)

     

    인터페이스와 다형성

    > 추상클래스를 Swift에서 어떻게 녹여내는게 좋을까?

    > Protocol extension ??


    05 추상화와 유연성

    추상화의 힘

    추상화를 거친 도메인의 장점

    1. 세부적 내용을 무시한 채 상위 정책을 쉽고 간단히 표현 가능

    2. 상위 정책을 표현하면 기본 구조를 수정하지 않고도 새로운 기능을 쉽게 추가, 확장해서 설계를 유연하게 만듬

     

    유연한 설계

    기존의 코드를 수정하지 않고 새로운 클래스를 추가하는 것만으로 앱의 기능을 확장 -> 컨텍스트 독립성 (context independency)

     

    추상 클래스와 인터페이스 트레이드 오프

    구현과 관련된 모든 것들이 트레이드 오프의 대상이 될 수 있다.

    struct NoneDiscountPolicy: DiscountPolicy {
        var conditions: [DiscountCondition] = []
        
        func discountAmount(_ screening: Screening) -> Money {
            return Money.makeZero()
        }
    }

     

    코드 재사용

    합성 (composition) : 상속보다 더 좋은 방향으로 지향, 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법

     

    상속

    문제점

    - 캡슐화를 위반 (가장 큰 문제)

       : 추상 클래스처럼 자식 클래스에 노출되기도 하고 부모 클래스 수정시 자식클래스도 함께 변경될 가능성 높음, 강하게 결합

    - 설계가 유연하지 않음

       : 컴파일 시점에 부모-자식 관계 결정됨, 실행시점에 객체 종류 변경 불가능 (합성인 경우 추상화된 프로퍼티를 다른 클래스로 바꾸는 동작이 가능)

    extension Movie { 
        mutating func changeDiscountPolicy(by discountPolicy: DiscountPolicy) {
            self.discountPolicy = discountPolicy
        }
    }

    > discountPolicy 가 protocol 으로 내부 구현이 숨겨져 있으므로 다음과 같이 변경이 가능하다.

     

    합성

    합성 : 인터페이스에 정의된 메세지를 통해서만 코드를 재사용하는 방법

     

    상속 관계는 부모 클래스의 코드와 자식 클래스의 코드를 컴파일 시점에 하나의 단위로 강하게 결합하는데 비해,

    합성 관계는 인터페이스를 통해 약하게 결합

     

    단순한 코드 재사용 -> 합성 

    > protocol extension 을 이용해 볼 수 있을것 같다.

    다형성을 위해 인터페이스를 재사용하는 경우 -> 상속과 합성 조합

    > protocol extension 과 override 을 우선적으로 사용

    > protocol extension 에 {} 처럼 빈 구현을 두고 필요하면 override 구현도 가능

     

    코드보기 Github

     

    iOS-Swift 관점으로 교재를 공부하고 생각을 남기는 목적입니다.
    Reference By: 오브젝트

Designed by Tistory.