apple/DesignPattern & Architecture

[Swift] Builder Pattern

lgvv 2022. 4. 22. 08:00

Builder Pattern

 

✅ Builder Pattern

 

아래의 문서를 구입하여 영어 문서를 번역하고 이해한 것을 바탕으로 글을 작성하고 있습니다.

 

https://www.raywenderlich.com/books/design-patterns-by-tutorials/v3.0/chapters/9-builder-pattern

 

Design Patterns by Tutorials, Chapter 9: Builder Pattern

The builder pattern allows the creation of complex objects step-by-step, instead of all at once, via an initializer. For example, you can use this pattern to implement a “hamburger builder”. The product could be a “hamburger” model, which has input

www.raywenderlich.com

 

 

✅ Builder Pattern

빌더 패턴을 사용하면 이니셜라이저를 통해 모든 입력을 미리 요구하는 대신 단계별로 입력을 제공함으로써 복잡한 객체를 생성할 수 있습니다. 이 패턴에는 3가지 부분으로 구성됩니다.

Builder Pattern

1. director는 입력을 accept하고 builder와 조정하는 역할을 합니다. 이것은 일반적으로 viewController와 ViewController에서 사용되는 helper 클래스입니다.

2. product는 생성될 복합 객체입니다. 원하는 reference semantics(참조 의미)에 따라서 struct 또는 class 혹은 클래스가 될 수 있습니다. 일반적으로는 model이지만 사용하는 경우에 따라 어떤 타입이든 될 수 있습니다.

3. builder는 단계별 입력을 accept하고 product 생성을 처리합니다. 이것은 종종 클래스이므로 참조에 의해서 재사용 될 수도 있습니다.

 

✅ When should you use it?

 

일련의 단계를 사용하여 복잡한 객체를 생성하려는 경우에 빌더 패턴을 사용하세요!

 

이 패턴은 multiple inputs이 필요한 경우에 특히 효과적입니다. builder는 이러한 입력이 product를 만드는데 사용되는 방식을 추상화하고 director가 제공하는 순서대로 입력을 accept합니다! 

 

예를 들어서 이 패턴을 이용해서 "hamburger builder"를 구현할 수 있습니다.

product는 "hamburger" 고기 선택, 토핑 및 소스와 같은 input이 있는 모델이 될 수 있습니다.

director는 employee 햄버거를 만드는 방법을 알고 있는 객체일 수도 있고, 사용자의 입력을 받는 viewController일 수도 있습니다.

 

따라서 "hamburger builder"는 고기 선택, 토핑 및 소스를 원하는 순서대로 수락하고 요청 시 햄버거를 만들 수 있습니다. 

 

 

✅ Playground example

위에서 했던 hamburger builder에 대한 예제를 구현합니다.

 

 

import Foundation

// MARK: - Product
// 1
public struct Hamburger {
  public let meat: Meat
  public let sauce: Sauces
  public let toppings: Toppings
}

extension Hamburger: CustomStringConvertible {
  public var description: String {
    return meat.rawValue + " burger"
  }
}

// 2
public enum Meat: String {
  case beef
  case chicken
  case kitten
  case tofu
}

// 3
public struct Sauces: OptionSet {
  public static let mayonnaise = Sauces(rawValue: 1 << 0)
  public static let mustard = Sauces(rawValue: 1 << 1)
  public static let ketchup = Sauces(rawValue: 1 << 2)
  public static let secret = Sauces(rawValue: 1 << 3)

  public let rawValue: Int
  public init(rawValue: Int) {
    self.rawValue = rawValue
  }
}

// 4
public struct Toppings: OptionSet {
  public static let cheese = Toppings(rawValue: 1 << 0)
  public static let lettuce = Toppings(rawValue: 1 << 1)
  public static let pickles = Toppings(rawValue: 1 << 2)
  public static let tomatoes = Toppings(rawValue: 1 << 3)

  public let rawValue: Int
  public init(rawValue: Int) {
    self.rawValue = rawValue
  }
}

1. 첫번째로 Hamburger에 대한 속성이 있는 meat, sauce, toppings 프로퍼티를 정의합니다.

햄버거가 만들어지면 let 프로퍼티를 통해서 components(구성요소)가 변경되는 것을 허락하지 않을 수 있습니다.Hamburger는 CustomStringConvertible을 준수합니다.

2. enum으로 선언된 Meat를 정의합니다. 각 햄버거에는 오로지 정확하게 하나의 고기 선택만이 있어야 합니다. 

3. OptionSet으로 Sauces를 정의합니다. 이렇게 하면 여러 소스를 함께 결합할 수 있습니다. 

4. 마찬가지로 OptionSet으로 Toppings를 저장합니다. 

 

다음으로는 Builder를 구성하는 코드입니다.

// MARK: - Builder
public class HamburgerBuilder {

  // 1
  public private(set) var meat: Meat = .beef
  public private(set) var sauces: Sauces = []
  public private(set) var toppings: Toppings = []

  // 2
  public func addSauces(_ sauce: Sauces) {
    sauces.insert(sauce)
  }

  public func removeSauces(_ sauce: Sauces) {
    sauces.remove(sauce)
  }

  public func addToppings(_ topping: Toppings) {
    toppings.insert(topping)
  }

  public func removeToppings(_ topping: Toppings) {
    toppings.remove(topping)
  }

  public func setMeat(_ meat: Meat) {
    self.meat = meat
  }

  // 3
  public func build() -> Hamburger {
    return Hamburger(meat: meat,
                     sauce: sauces,
                     toppings: toppings)
  }
}

 

여기에는 미묘하게 중요한 부분이 몇가지 있습니다!

1. meat, sauce, toppings는 정확하게 Hamburger를 위한 프로퍼티로 선언되어 있습니다. Hamburger와 달리 변경을 할 수 있도록 var를 이용하여 선언합니다. 또한 private(set)을 각각에서 지정할 수 있도록 HamburgerBuilder를 직접적으로 등록합니다.

2. private(set)을 사용하여 각각의 프로퍼티를 선언했기 때문에, 그들의 변화를 public 메소드를 통해 제공해야 합니다. 이를 우리는 addSauces, removeSauces, addToppings, removeToppings, setMeats 등을 통해서 할 수 있습니다.

3. 마지막으로 builde()를 선택을 통해 Hamburger를 생성하도록 정의합니다.

 

private(set)은 public setter 메소드를 consumers가 사용하도록 강제합니다. 이를 통해 builder가 프로퍼티가 세팅되기 전에 validation(유효성) 검사를 수행할 수 있도록 허락합니다.

 

예를 들어 meat를 설정하기 전에 사용 가능한 지 확인합니다.

private var soldOutMeats: [Meat] = [.kitten]

 

 고기가 매진되면 setMeat()를 호출할 때마다 오류가 발생합니다. 이를 위해 사용자 정의 오류를 선언해야 합니다. HambergerBuilder코드 바로 뒤에 이를 추가합니다.

public enum Error: Swift.Error {
  case soldOut
}

마지막으로 setMeat를 바꿉니다.

public func setMeat(_ meat: Meat) throws {
  guard isAvailable(meat) else { throw Error.soldOut }
  self.meat = meat
}

public func isAvailable(_ meat: Meat) -> Bool {
  return !soldOutMeats.contains(meat)
}

 

kitten 고기를 설정하려고 하면 이제 soldOut이라는 오류가 표시됩니다. 

 

다음으로는 director를 선언해야 합니다.

// MARK: - Director
public class Employee {

  public func createCombo1() throws -> Hamburger {
    let builder = HamburgerBuilder()
    try builder.setMeat(.beef)
    builder.addSauces(.secret)
    builder.addToppings([.lettuce, .tomatoes, .pickles])
    return builder.build()
  }

  public func createKittenSpecial() throws -> Hamburger {
    let builder = HamburgerBuilder()
    try builder.setMeat(.kitten)
    builder.addSauces(.mustard)
    builder.addToppings([.lettuce, .tomatoes])
    return builder.build()
  }
}

Employee는 createCombo1 및 createKittenSpecial 두 개의 버거를 만드는 방법을 알고 있습니다.

 

아래는 실제로 코드가 작동하는 부분입니다.

// MARK: - Example
let burgerFlipper = Employee()

if let combo1 = try? burgerFlipper.createCombo1() {
  print("Nom nom " + combo1.description)
}

여기에서 Employee 인스턴스를 생성하여 burgerFlipper 변수를 만들고 createCombo1을 호출합니다. 

Nom nom beef burger

콘솔에 위와 같이 print된 것을 확인할 수 있습니다. 

 

다음으로 아래 코드를 추가합니다.

if let kittenBurger = try?
  burgerFlipper.createKittenSpecial() {
  print("Nom nom nom " + kittenBurger.description)

} else {
  print("Sorry, no kitten burgers here... :[")
}

kitten 버거는 매진되었기 때문에 어떻게 처리 되는지 보겠습니다.

Sorry, no kitten burgers here... :[

콘솔에 위와 같은 메시기자 print됩니다.

 

 

✅ What should you be careful about?

 

빌더 패턴을 일련의 단계를 사용하여 여러 입력이 필요한 복잡한 제품을 만드는데 가장 적합합니다. 제품에 여러 입력이필요 없거나 단계별로 생성할 수 없는 경우 빌더 패턴이 가치보다 문제점이 더 커질 수 있습니다.

 

대신, convenience init을 이용하여 product를 제공하는 것을 고려해보세요!! 

 

 

Tutorial project

주석으로 공부하였는데, 꼭 확인하자. 빌더 패턴을 그리 어려운게 아니지만 코드를 사용하는 스타일에서 throw 처럼 에러를 처리하거나 테이블 뷰를 더 다채롭게 사용하고 있어서 배울 점이 아주 많다.!

https://github.com/lgvv/DesignPattern/tree/main/builder-pattern/RabbleWabble

 

GitHub - lgvv/DesignPattern: ✨ 디자인 패턴을 공부합니다!

✨ 디자인 패턴을 공부합니다! Contribute to lgvv/DesignPattern development by creating an account on GitHub.

github.com

 

 

 Keys points

 

이번에는 빌더 패턴에 대해서 공부했다.

 

 1. 빌더 패턴은 일련의 순서가 있는 복잡한 객체를 만드는데 아주 좋은 패턴이다.

director, product and builder가 빌더 패턴에 포함된다.

 2. director는 input을 받고 builder와 함께 협력한다.

      product는 생성된 복잡한 객체이다.

      builder는 일련의 순서로 인풋을 처리하고 product를 생성한다.