본문 바로가기
iOS/Design Pattern

Design Pattern - Builder Pattern

by HaningYa 2020. 8. 9.
728x90

Builder Pattern

복잡한 Object 에 대해 initializer에서 필요한 모든 input을 입력하지 않고 단계별로 input을 줄 수 있게 한다.

출처: design_pattern_by_tutorials (raywenderlich)

빌더 패턴은 3가지 부분으로 나눠진다.

  • director: input 을 accept 하고 builder 와 coordinate 한다.
    대부분 view controller에 의해 사용되는 view controller 나 helper class 가 담당한다.
  • product: 생성하기 힘든 복잡한 객체이다. struct 나 class 가 될 수 있고 대부분 model 역할이다. 그런데 use case 에 따라 아무 타입이나 다 될 수 있다.
  • builder: 단계별로 input을 받고 product 의 생성을 처리한다. 대부분 class 이고 refernce 에 의해 재사용될 수 있다.

Builder Pattern - 언제 사용해야 할까

  • 복잡한 객체를 단계별로 만들어 나갈때 사용한다.
  • 이 패턴은 특히 여러개의 입력값 (multiple input) 이 있을때 사용된다.
  • builder 는 어떻게 input들이 객체 생성을 위해 사용되는 지를 추상화 하고 input을 director 가 제공하는 순서에 관계없이 받는다(accept)
  • 예를 들어 Builder Pattern 으로 "hamburger builder"를 만들 수 있다.
    • product: 햄버거 model (고기나 야채 치즈 등 여러가지 선택할 수 있는 복잡한 모델)
    • director: 종업원 객체 (어떻게 햄버거를 만드는지 아는 객체) 또는 사용자로 부터 input 을 받는 view controller

Builder Pattern 예제

Product 코드

  1.  meat, sauce, toppings 프로퍼티를 가진 Hamburger를 정의한다.
    햄버거가 한번 만들어 지면 그 구성을 바꿀 수 없으므로 let 으로 프로퍼티를 선언한다.
    CustomStringConvertible 을 준수하여 나중에 어떤 햄버거인지 print 하게 한다.
  2. Meat 를 enum 으로 선언하여 하나의 종류의 고기만 선택할 수 있게 한다.
  3. Sauces 는 OptionalSet 으로 선언하여 여러가지 조합으로 선택할 수 있게 한다.
  4. 같은 이유로 Toppings 도 OptionalSet 으로 선언한다.
import Foundation

//Product
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"
    }
}

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

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
    }
}

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
	}
}

Builder 코드

  1. meat, sauces, toppings를 햄버거 input 에 맞게 선언한다. 그리고 product 코드와 다르게 var 로 선언한다. 그리고 각각 private (set)으로 HamburgerBuilder 만 직접적으로 set 할 수 있게 만든다.
  2. 모든 property 를 private(set)으로 정했기 때문에 public method가 필요하다. addSauces(_:), removeSauces(_:), addToppings(_:), removeToppings(_:), setMeat(_:) 의 public method 를 정의한다.
  3. 선택된 대로 Hamburger를 만들 수 있는 build() 를 정의한다.
public class HamburgerBuilder {
	public private(set) var meat: Meat = .beef
    public private(set) var souces: Sauces = []
    public private(set) var toppings: Toppings = []
    
    public func addSauces(_ sauce: Sauces) {
    	sauces.insert(sauce)
    }
    
    public func removeSauces(_ sauce: Sauces) {
    	sauces.remove(sauce)
    }
    
    public func addToppings(_ topping: Toppings) {
    	toppings.inert(topping)
    }
    
    public func removeToppings(_ topping: Toppings) {
    	toppings.remove(topping)
    }
    
    public func setMeat(_ meat: Meat) {
    	self.meat = meat
    }
    
    public func build() -> Hamburger {
    	return Hamburger(meat: meat, sauce : sauces, toppings: toppings)
    }
}

위 코드에서 private(set)을 통해 consumer 가 강제로 public setter 메소드를 쓰게 했다.

이 방법은 builder 가 값을 정하기 전에 validation을 할 수 있게 한다.

예를들어 meat 의 종류중 품절된게 있는 경우

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

public enum Error: Swift.Error {
	case soldOut
}

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)
}

기존에 setMeat 메소드에 isAvailable 을 통해 Validation을 할 수 있다.


Director 코드

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

Employee 는 위 두 함수처럼 두종류의 버거를 만들줄 안다. 


코드 실행해서 결과보기

let burgerFlipper = Employee()

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

if let kittenBurger = try?
	burgerFlipper.createKittenSpeial() {
    	print("Nom nom nom " + kittenBurger.description)
    } else {
    	print("no kitten burger here")
    }

combo1의 경우 성공적으로 object 가 만들어지는데 kittenBurger 의 경우 Kitten 이 품절이므로 no kitten burger here 가 출력된다.


Builder Pattern 사용시 주의할 점

  • product 가 여러개의 input을 필요로 하지 않거나 단계별로 만들어져야 되는 object 가 아니라면 이 패턴을 쓸 가치가 없다.
  • 위의 경우 그냥 initializer 를 통해 product 를 만드는게 좋다.

 

 

 

728x90

'iOS > Design Pattern' 카테고리의 다른 글

Design Pattern - Factory Pattern  (0) 2020.08.25
Design Pattern - Model View ViewModel Pattern  (2) 2020.08.24
Design Pattern - Observer Pattern  (0) 2020.08.09
Design Pattern - Memento Pattern  (0) 2020.08.09
Design Pattern - Singleton Pattern  (0) 2020.08.09

댓글