jay's devnote
  • README
  • 자료구조 & 알고리즘
    • 자료구조
    • 알고리즘
  • 디자인 패턴
    • 디자인 패턴 원칙, 요약, 분류
    • IS-A 와 HAS-A
    • 전략 패턴
    • 옵저버 패턴
    • 데코레이터 패턴
    • 팩토리 패턴
    • 싱글턴 패턴
    • 커맨드 패턴
    • 어댑터, 퍼사드 패턴
    • 템플릿 메소드 패턴
    • 반복자, 컴포지트 패턴
    • 상태 패턴
    • 프록시 패턴
    • 복합 패턴
  • WWDC
    • 2015, Building Better Apps with Value Types in Swift
    • 2015, Protocol-Oriented Programming in Swift
    • 2016, Understanding Swift Performance
    • 2016, Protocol and Value Oriented Programming in UIKit Apps
    • 2017, Engineering for Testability
    • 2018, High Performance Auto Layout
    • 2018, Testing Tips & Tricks
    • 2020, Advances in UICollectionView
    • 2020, Lists in UICollectionView
  • 패러다임
    • 객체지향 프로그래밍, SOLID 원칙
      • SRP, 단일 책임 원칙
      • OCP, 개방 폐쇄 원칙
      • LSP, 리스코프 치환 원칙
      • ISP, 인터페이스 분리 원칙
      • DIP, 의존성 역전 원칙
    • 만들면서 느껴보는 POP
    • Swift로 함수형 프로그래밍 시작하기
  • 아키텍쳐
    • ReactorKit
      • Pulse(EN)
      • Pulse(KR)
    • Coordinator Pattern
  • iOS
    • Safari로 웹뷰의 세션/쿠키 정보 확인하기
    • App Icon 동적으로 변경하기
    • WKDataDetectorTypes의 데이터 탐지
    • Xcode에서 메모리 누수 확인하기
    • 개발 인증서 관리하기
    • required init?(coder: NSCoder)
    • UIFontMetrics 와 UIFont.preferredFont
    • 제약조건을 줄여주는 UIStackView
    • UICollectionView.CellRegistration<Cell, Item>
  • Swift
    • Swift API Design Guidelines
    • 패턴 매칭
    • allSatisfy()
    • 생성자
    • 프로토콜의 동적 디스패치와 정적 디스패치
    • Swift 문법 정리
  • RxSwift
    • RxSwift 핸드북
    • Just, From, Of
    • withLatestFrom
  • SwiftUI
    • SwiftUI에서의 마크다운 문법
    • @State, @Binding
    • ObservableObject, @ObservedObject, @Published
    • @ObservedObject vs @StateObject
  • Git
    • Git gitignore
    • Github API Rate limit
    • GitKraken(깃크라켄) 활용하기
    • GitKraken으로 Git-flow 활용하기
  • Etc
    • Struct을 [String: Any]로 변환할 때, Encodable의 Extension을 사용 해야 하나요?
    • Podfile, Dependency Rule(SPM)
    • 맥으로 고정 IP 연결하는 방법
    • SwiftPlantUML으로 UML 다이어그램 쉽게 그리기
    • Playground 가 열리지 않는 오류 해결하기
    • CocoaPods 제거하기
  • Python
    • 파이썬과 스위프트 문법 비교
    • 파이썬과 스위프트 문법 요약
  • Firebase
    • Storage를 API처럼 사용해보기
    • RealTime Database를 API처럼 사용해보기
Powered by GitBook
On this page
  • 1. 스타벅스 메뉴를 만듭니다.
  • 2. 변수와 상속을 사용해서 변경해보기
  • 3. 상속을 사용한 설계의 단점 알아보기
  • 4. 데코레이터 패턴 설계하기 1
  • 5. 데코레이터 패턴 설계하기 2
  • 6. 데코레이터 패턴 구현하기
  • 7. 데코레이터 패턴 정리하기
  • 8. 커피 메뉴에 사이즈 추가하기
  • 9. 개방 폐쇄 원칙(OCP)
  • Reference
  1. 디자인 패턴

데코레이터 패턴

Previous옵저버 패턴Next팩토리 패턴

Last updated 2 years ago

  • 객체에 추가 요소를 동적으로 더할 수 있습니다.

  • 데코레이터를 사용하면 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있습니다.

1. 스타벅스 메뉴를 만듭니다.

  • 처음엔 Beverage(음료) 인터페이스를 통해 하우스블렌드, 다크로스트 원두와 디카페인 그리고 에스프레소라는 큰 종류로 나뉘었어요.

  • 그런데, 우유/데운우유/두유/휘핑크림/모카...와 같은 메뉴 추가사항이 늘어납니다. 이때 추가된 메뉴별로 비용이 추가되기 때문에 여기서 상속을 사용하면 서브클래스가 폭발하게 됩니다.

protocol Beverage {
    var description: String { get }
    
    func getDescription() -> String
    func cost() -> Double
}

class HouseBlend: Beverage {
    let description: String = "하우스 블렌드 원두"
    
    func getDescription() -> String {
        return description
    }
    
    func cost() -> Double {
        return 2.49
    }
}

class DarkRoast: Beverage {
    let description: String = "다크 로스트 원두"
    
    func getDescription() -> String {
        return description
    }
    
    func cost() -> Double {
        return 2.49
    }
}

class Decaf: Beverage {
    let description: String = "디카페인"
    
    func getDescription() -> String {
        return description
    }
    
    func cost() -> Double {
        return 2.29
    }
}

class Espresso: Beverage {
    let description: String = "에스프레소"
    
    func getDescription() -> String {
        return description
    }
    
    func cost() -> Double {
        return 1.99
    }
}
/*
 milk 우유
 steamed milk 데운 우유
 soy milk 두유
 whip 휘핑크림
 mocha 모카
 ...
 */
class HouseBlendWithMilk: Beverage { ... }
class HouseBlendWithSteamedMilk: Beverage { ... }
class HouseBlendWithSoy: Beverage { ... }
class HouseBlendWithMocha: Beverage { ... }
class HouseBlendWithWhip: Beverage { ... }
class HouseBlendWithMilkandSoy: Beverage { ... }
class HouseBlendWithSteamedMilkandWhip: Beverage { ... }
class HouseBlendWithSoyandWhip: Beverage { ... }
class HouseBlendWithMochaandWhip: Beverage { ... }
class HouseBlendWithWhipandMilkandSoy: Beverage { ... }

class DarkRoastWithMilk: Beverage { ... }
class DarkRoastWithSteamedMilk: Beverage { ... }
class DarkRoastWithSoy: Beverage { ... }
class DarkRoastWithMocha: Beverage { ... }
class DarkRoastWithWhip: Beverage { ... }
class DarkRoastWithMilkandSoy: Beverage { ... }
class DarkRoastWithSteamedMilkandWhip: Beverage { ... }
class DarkRoastWithSoyandWhip: Beverage { ... }
class DarkRoastWithMochaandWhip: Beverage { ... }
class DarkRoastWithWhipandMilkandSoy: Beverage { ... }

class EspressoWithMilk: Beverage { ... }
class EspressoWithSteamedMilk: Beverage { ... }
class EspressoWithSoy: Beverage { ... }
class EspressoWithMocha: Beverage { ... }
class EspressoWithWhip: Beverage { ... }
class EspressoWithMilkandSoy: Beverage { ... }
class EspressoWithSteamedMilkandWhip: Beverage { ... }
class EspressoWithSoyandWhip: Beverage { ... }
class EspressoWithMochaandWhip: Beverage { ... }
class EspressoWithWhipandMilkandSoy: Beverage { ... }

class DecafWithMilk: Beverage { ... }
class DecafWithSteamedMilk: Beverage { ... }
class DecafWithSoy: Beverage { ... }
class DecafWithMocha: Beverage { ... }
class DecafWithWhip: Beverage { ... }
class DecafWithMilkandSoy: Beverage { ... }
class DecafWithSteamedMilkandWhip: Beverage { ... }
class DecafWithSoyandWhip: Beverage { ... }
class DecafWithMochaandWhip: Beverage { ... }
class DecafWithWhipandMilkandSoy: Beverage { ... }

2. 변수와 상속을 사용해서 변경해보기

  • 부모 클래스에 각 추가옵션의 Bool 타입을 정의하고, get/set 메소드 또한 정의합니다.

  • 해당 Bool 타입의 여부에 따라서 cost() 메소드를 통해 비용을 추가해줍니다.

  • 자식 클래스에서는 본인 메뉴의 비용 + super.cost()를 호출하여 비용을 추가합니다.

class Beverage {
    private var description: String
    private var milk: Bool = false
    private var soy: Bool = false
    private var mocha: Bool = false
    private var whip: Bool = false
    
    init(description: String) {
        self.description = description
    }
    
    func getDescription() -> String {
        self.description
    }
    
    private func hasMilk() -> Bool { self.milk }
    private func hasSoy() -> Bool { self.soy }
    private func hasMocha() -> Bool { self.mocha }
    private func hasWhip() -> Bool { self.whip }
    
    func setMilk(_ b: Bool) { self.milk = b }
    func setSoy(_ b: Bool) { self.soy = b }
    func setMocha(_ b: Bool) { self.mocha = b }
    func setWhip(_ b: Bool) { self.whip = b }
    func cost() -> Double {
        var condimentCost: Double = 0.0 //첨가물 비용
        let milkCost: Double = 2.0
        let soyCost: Double = 3.0
        let mochaCost: Double = 2.5
        let whipCost: Double = 1.0
        
        if hasMilk() { condimentCost += milkCost }
        if hasSoy() { condimentCost += soyCost }
        if hasMocha() { condimentCost += mochaCost }
        if hasWhip() { condimentCost += whipCost }
        
        return condimentCost
    }
    
}
class HouseBlend: Beverage {
    override func cost() -> Double {
        return 2.49 + super.cost()
    }
}

class DarkRoast: Beverage {
    override func cost() -> Double {
        return 2.49 + super.cost()
    }
}

class Decaf: Beverage {
    override func cost() -> Double {
        return 2.29 + super.cost()
    }
}

class Espresso: Beverage {
    override func cost() -> Double {
        return 1.99 + super.cost()
    }
}
let cafelatte = Espresso(description: "카페라떼")
cafelatte.setMilk(true)

//에스프레소 1.99 + 우유 2.0
print(cafelatte.getDescription(), cafelatte.cost())

let cafeMocha = Espresso(description: "카페모카")
cafeMocha.setMocha(true)
cafeMocha.setWhip(true)

//에스프레소 1.99 + 모카 2.5 + whip 1.0
print(cafeMocha.getDescription(), cafeMocha.cost())
    
    
/*
카페라떼 3.99
카페모카 5.49
*/

3. 상속을 사용한 설계의 단점 알아보기

  • 첨가물 가격이 바뀔 때마다 기존 코드를 수정해야 합니다.

  • 첨가물의 종류가 많아지면 새로운 get/set 메소드를 추가해야 하고, 부모클래스의 cost() 메소드 또한 고쳐야 합니다.

  • 새로운 음료가 나올 수 있어요. 아이스티같은. 근데 아이스티는 hasWhip()같은 메소드는 상속받지 않아야 하지만, 상속을 받을 것입니다. 이건 Strategy Pattern을 공부할 때도 말했듯이, 고무 오리가 날아다닌다던지 하는 예상치 못한 치명적인 오류를 발생시킬 수 있어요.

  • 고객이 더블 모카를 주문하면 어떻게 하나요 ? ( 현재는 hasMocha()로만 작업되어있으므로 주문 불가하죠. )

  • 결론적으로 우리의 목표는 기존 코드는 건드리지 않고, 확장으로만 새로운 행동을 추가하는 것입니다.

4. 데코레이터 패턴 설계하기 1

  • Decorator(장식)의 슈퍼클래스는 자신이 장식하고 있는 객체의 슈퍼클래스와 같습니다.

  • 한 객체를 여러 개의 데코레이터로 감쌀 수 있습니다.

  • Decorator(장식)는 자신이 감싸고 있는 객체와 같은 슈퍼클래스를 가지고 있기에 원래 객체가 들어갈 자리에 데코레이터 객체를 넣어도 상관없습니다.

  • Decorator(장식)는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 일 말고도 추가 작업을 수행할 수 있습니다.

  • 객체는 언제든지 감쌀 수 있으므로 실행 중에 필요한 데코레이터를 마음대로 적용할 수 있습니다.

5. 데코레이터 패턴 설계하기 2

  • 이해를 돕기위해 다이어그램을 참고하자.

  • 중요한 CondimentDecorator에서 Beverage를 상속하고 있다는 점과 구성(Composition)으로 Beverage를 가지고 있다는 점이다.

  • 위에서 상속 안좋다고 그러고 왜 상속을 쓰냐고 할 수도 있다만 이건 다른 너낌이다. 왜냐면 이따가 코드를 보면 알겠지만, Berverage 객체를 생성하고 그 녀석을 Decorator(장식)로 감싸서 할당해주기 때문이다.

  • 즉, 기존의 상속처럼 부모 클래스의 행동을 상속하기 위한 상속이 아니라 생성한 Berverage 객체에 데코레이터로 감싼 객체가 들어가야되서 상속을 받고 있는 것이다.

  • 그리고 또 다이어그램을 보면 있는 우유, 모카, 두유, 휘핑크림같은 Decorator(장식)들이 CondimentDecorator를 상속한다.

  • 이건 무슨의미냐면, CondimentDecorator는 부모 클래스인 Berverage 객체를 구성으로 가지고 있다. 그리고 메뉴 주문에 따라 모카로 래핑, 모카 또 래핑, 휘핑 래핑, 우유 래핑... 같은 식으로 계속 래핑이 되는데 최종 비용을 계산하려면 Berverage 객체가 필요하다.

  • 자세한 내용은 아래의 구현된 코드를 보자.

6. 데코레이터 패턴 구현하기

  • Beverage 클래스가 있고, 서브 클래스로 HouseBlend, DarkRoast, Decaf, Espresso가 있다. 이 클래스들은 내부에 description, cost() 이 구현되어 있다.

  • CondimentDecorator 는 Beverage 을 상속하며, Beverage 객체를 Composition(구성) 으로 가지고 있다. 따라서 Beverage 혹은 Beverage 의 서브 클래스들에 접근이 가능하다.

  • Decorator(장식)들은 모카, 두유, 우유, 휘핑크림이 있는데 CondimentDecorator를 상속하고 있고 생성자를 통해 Beverage 를 받는다.

  • 또, Decorator(장식)들은 getDescription(), cost()을 Beverage 객체를 통해서 누적으로 계산할 수 있다.

class Beverage {
    var description: String = "제목 없음"
    
    func getDescription() -> String {
        return self.description
    }
    
    func cost() -> Double {
        return 0.0
    }
}

class CondimentDecorator: Beverage {
    var beverage = Beverage()
}

class HouseBlend: Beverage {
    override init() {
        super.init()
        description = "하우스 블렌드 원두"
    }
    
    override func cost() -> Double {
        return 2.49
    }
}

class DarkRoast: Beverage {
    override init() {
        super.init()
        description = "다크 로스트 원두"
    }
    
    override func cost() -> Double {
        return 2.49
    }
}

class Decaf: Beverage {
    override init() {
        super.init()
        description = "디카페인"
    }
    
    override func cost() -> Double {
        return 2.29
    }
}

class Espresso: Beverage {
    override init() {
        super.init()
        description = "에스프레소"
    }
    
    override func cost() -> Double {
        return 1.99
    }
}
class Mocha: CondimentDecorator {
    init(beverage: Beverage) {
        super.init()
        self.beverage = beverage
    }
    
    override func getDescription() -> String {
        return beverage.getDescription() + ", 모카"
    }
    
    override func cost() -> Double {
        return beverage.cost() + 2.5
    }
}

class Soy: CondimentDecorator {
    init(beverage: Beverage) {
        super.init()
        self.beverage = beverage
    }
    
    override func getDescription() -> String {
        return beverage.getDescription() + ", 두유"
    }
    
    override func cost() -> Double {
        return beverage.cost() + 3.0
    }
}

class Milk: CondimentDecorator {
    init(beverage: Beverage) {
        super.init()
        self.beverage = beverage
    }
    
    override func getDescription() -> String {
        return beverage.getDescription() + ", 우유"
    }
    
    override func cost() -> Double {
        return beverage.cost() + 2.0
    }
}

class Whip: CondimentDecorator {
    init(beverage: Beverage) {
        super.init()
        self.beverage = beverage
    }
    
    override func getDescription() -> String {
        return beverage.getDescription() + ", 휘핑크림"
    }
    
    override func cost() -> Double {
        return beverage.cost() + 1.0
    }
}
/*
 <메뉴판>
 
 [메인메뉴]
 2.49 하우스블렌드
 2.49 다크로스트
 2.29 디카페인
 1.99 에스프레소
 
 [첨가물]
 2.0 우유
 3.0 두유
 2.5 모카
 1.0 휘핑크림
 
 */

var beverage: Beverage = Espresso()
print("\(beverage.getDescription()) $\(beverage.cost())")
//에스프레소 $1.99

var beverage2: Beverage = DarkRoast()
beverage2 = Mocha(beverage: beverage2)
beverage2 = Mocha(beverage: beverage2)
beverage2 = Whip(beverage: beverage2)
print("\(beverage2.getDescription()) $\(beverage2.cost())")
//다크 로스트 원두, 모카, 모카, 휘핑크림 $8.49

var beverage3: Beverage = HouseBlend()
beverage3 = Soy(beverage: beverage3)
beverage3 = Mocha(beverage: beverage3)
beverage3 = Whip(beverage: beverage3)
print("\(beverage3.getDescription()) $\(beverage3.cost())")
//하우스 블렌드 원두, 두유, 모카, 휘핑크림 $8.99

7. 데코레이터 패턴 정리하기

  • 아래의 코드는 아래의 그림을 코드로 구현해서 실행하는 부분이다.

  • 그림을 보면 계속 모카, 휘핑 크림 추가 처럼 첨가물을 계속 실행 중에 추가할 수 있게 되어 있다.

  • 왜? 데코레이터가 Berverage를 상속해서 타입이 같고, 모카/우유 애들은 CondimentDecorator를 상속하고 있으니까 그렇다. 즉, 상속을 행동을 받으려고 한 것이 아니라 타입을 맞추려고 한 것이다.

  • 또, 생성자로 구성으로 가지고 있는 beverage을 입력받아 기존에 래핑 되어있는 데이터에 접근이 가능해서 그 값에 추가해서 최종결과를 리턴해준다.

  • 이런식이면, 예를들어 첨가물이 새롭게 추가된다고 했을 때 기존 코드는 건드리지 않고 데코레이터를 상속한 첨가물을 새로 만들면 된다. 그래서 실행 중에 마음대로 조합해서 사용할 수 있다.

  • 즉, 목표대로 기존 코드는 건드리지 않고(변경에는 닫혀있고), 새로운 행동을 추가(확장에는 열려있다)할 수 있는 것이다.

  • 유의할 점은 아래의 코드에 나와 있는 것 처럼 한 번 DarkRoast를 Beverage로 감싸고 나면, 그게 DarkRoast인지 뭔지 알 수가 없다는 부분이다.

var darkroast: Beverage = DarkRoast()
darkroast = Mocha(beverage: darkroast)
darkroast = Whip(beverage: darkroast)
print("\(darkroast.getDescription()) $\(darkroast.cost())")
//다크 로스트 원두, 모카, 휘핑크림 $5.99

8. 커피 메뉴에 사이즈 추가하기

  • 커피에 Tall, Grande, Venti 사이즈가 새로 생겼다.

  • 그리고 사이즈에 따라 첨가물도 그만큼 양이 추가될 것이므로 첨가물 가격도 다르게 받으려고 한다.

class Beverage {
    enum Size { case TALL, GRANDE, VENTI }
    var size: Size = .TALL
    var description: String = "제목 없음"
    
    func getDescription() -> String {
        return self.description
    }
    
    func cost() -> Double {
        return 0.0
    }
    
    func setSize(_ size: Size) {
        self.size = size
    }
    
    func getSize() -> Size {
        return self.size
    }
}
    
class CondimentDecorator: Beverage {
    var beverage = Beverage()
    
    override func getSize() -> Beverage.Size {
        return beverage.getSize()
    }
}
    
class Whip: CondimentDecorator {
    init(beverage: Beverage) {
        super.init()
        self.beverage = beverage
    }
    
    override func getDescription() -> String {
        return beverage.getDescription() + ", 휘핑크림"
    }
    
    override func cost() -> Double {
        var cost = beverage.cost()
        switch beverage.getSize() {
        case .TALL: cost += 1.0
        case .GRANDE: cost += 1.5
        case .VENTI: cost += 2.0
        }
        return cost
    }
}
    
var darkroast: Beverage = DarkRoast()
darkroast = Mocha(beverage: darkroast)
darkroast = Whip(beverage: darkroast)
print("\(darkroast.getDescription()) $\(darkroast.cost())")
//다크 로스트 원두, 모카, 휘핑크림 $5.99

var ventiDarkroast: Beverage = DarkRoast()
ventiDarkroast.setSize(.VENTI)
ventiDarkroast = Mocha(beverage: ventiDarkroast)
ventiDarkroast = Whip(beverage: ventiDarkroast)
print("\(ventiDarkroast.getDescription()) $\(ventiDarkroast.cost())")
//다크 로스트 원두, 모카, 휘핑크림 $6.99

9. 개방 폐쇄 원칙(OCP)

  • 클래스는 확장에는 열려있어야 하지만 변경에는 닫혀 있어야 한다는 원칙이다(Open Closed Principle).

  • 위에서 본 것처럼 유연성 면에서 보면 상속으로 확장하는 일은 별로 좋은 선택이 아니다.

  • 데코레이터 패턴을 사용하면 구성과 위임으로 실행 중에 새로운 행동을 추가할 수도 있다.

  • 데코레이터 패턴의 예시처럼 어떤 메뉴나 옵션이 하나 추가됐다고하면 기존 코드를 건드리지 않고, 그 옵션에 해당하는 데코레이터를 새로 만들어서 기존의 음료에 감싸주기만 하면 된다.

Reference

https://www.hanbit.co.kr/store/books/look.php?p_code=B6113501223