선택적인 매개변수가 많은 클래스의 경우, 점층적 생성자 패턴을 사용할 수 있습니다.
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
클라이언트 측에서 설정하기 원치 않는 매개변수까지 값을 지정해야 하고, 각 인자들의 의미가 무엇인지 알기 어렵습니다.
import Foundation
public class NutritionFacts {
private let servingSize: Int
private let servings: Int
private let calories: Int
private let fat: Int
private let sodium: Int
private let carbohydrate: Int
init(
servingSize: Int,
servings: Int,
calories: Int = 0,
fat: Int = 0,
sodium: Int = 0,
carbohydrate: Int = 0
) {
self.servingSize = servingSize
self.servings = servings
self.calories = calories
self.fat = fat
self.sodium = sodium
self.carbohydrate = carbohydrate
}
}
let cocaCola = NutritionFacts(servingSize: 240, servings: 8, fat: 35)
자바에서는 default 파라미터가 없지만 스위프트에서는 지원해 줘서 설정을 원하는 매개변수에만 값을 지정할 수 있습니다.
또한 argument label을 통해 어떤 매개변수에 값을 전달하는 지도 알기 쉽습니다.
자바빈즈 패턴은 매개변수가 없는 생성자로 객체를 만든 후, setter를 이용해 원하는 매개변수의 값을 설정하는 방식을 말합니다.
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
점층적 생성자 패턴과 달리 어떤 매개변수에 값을 설정하는지 알기 쉬워져서 읽기 쉬운 코드가 되었으며, 클라이언트가 원하는 값만 설정할 수 있습니다.
하지만 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전 객체를 사용하게 되는 것을 막을 수 없습니다. 또한 필드들을 final로 선언할 수가 없습니다.
public class NutritionFacts {
private var servingSize: Int = -1
private var servings: Int = -1
private var calories: Int = 0
private var fat: Int = 0
private var sodium: Int = 0
private var carbohydrate: Int = 0
init() { }
func set(servingSize: Int) {
self.servingSize = servingSize
}
func set(servings: Int) {
self.servings = servings
}
func set(calories: Int) {
self.calories = calories
}
func set(fat: Int) {
self.fat = fat
}
func set(sodium: Int) {
self.sodium = sodium
}
func set(carbohydrate: Int) {
self.carbohydrate = carbohydrate
}
}
let cocaCola = NutritionFacts()
cocaCola.set(servingSize: 240)
cocaCola.set(servings: 8)
cocaCola.set(fat: 35)
빌더 패턴에서는 필수 매개변수만으로 이루어진 빌더 생성자를 호출해 빌더 객체를 얻고, 빌더 객체의 setter 메서드들로 선택 매개변수들을 설정합니다. 마지막으로 build를 호출해 필요한 객체를 얻습니다.
빌더 패턴은 생성되는 객체를 불변으로 만들 수 있고, 빌더의 setter 메서드들을 method chaining 형식으로 연쇄적으로 호출할 수 있다는 장점이 있습니다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
또한 빌더 패턴은 위 예시 코드처럼 쓰고 읽기 쉽습니다.
가변인수 매개변수를 여러 개 사용할 수도 있게 되며, 빌더 하나로 다양한 객체를 만들 수도 있어 유연합니다. 빌더에서 일련번호를 채우는 등 추가 기능을 구현할 수도 있습니다.
객체 생성 전에 빌더부터 만들어야 해서 생성 비용이 클 수 있습니다.
또한 코드가 비교적 장황해서 매개변수 4개 정도 이상일 때 적합합니다.
import Foundation
public class NutritionFacts {
private let servingSize: Int
private let servings: Int
private let calories: Int
private let fat: Int
private let sodium: Int
private let carbohydrate: Int
public class Builder {
let servingSize: Int
let servings: Int
var calories = 0
var fat = 0
var sodium = 0
var carbohydrate = 0
init(servingSize: Int, servings: Int) {
self.servingSize = servingSize
self.servings = servings
}
func calories(_ calories: Int) -> Self {
self.calories = calories
return self
}
func fat(_ fat: Int) -> Self {
self.fat = fat
return self
}
func sodium(_ sodium: Int) -> Self {
self.sodium = sodium
return self
}
func carbohydrate(_ carbohydrate: Int) -> Self {
self.carbohydrate = carbohydrate
return self
}
func build() -> NutritionFacts {
return NutritionFacts(from: self)
}
}
init(from builder: Builder) {
servingSize = builder.servingSize
servings = builder.servings
calories = builder.calories
fat = builder.fat
sodium = builder.sodium
carbohydrate = builder.carbohydrate
}
}
let cocaCola = NutritionFacts.Builder(servingSize: 240, servings: 8)
.calories(100)
.sodium(35)
.build()
계층적으로 설계된 클래스에서의 빌더 패턴을 스위프트로 구현해 보았습니다.
먼저 Pizza 추상 클래스의 경우 내부에 Topping enum이 있어야 해서 (프로토콜이 아닌) 클래스로 구현하였습니다.
Pizza의 abstract builder의 경우 T가 자기 자신을 확장한 타입이어야 하는데, 이를 표현하기 위해 프로토콜과 associated type을 이용하였고, associated type이 자기 자신 프로토콜을 따르도록 제한하였습니다.
이렇게 한 이유는 스위프트 클래스는 재귀적 타입 한정을 이용한 제네릭을 지원하지 않았기 때문입니다. 더 좋은 방법이 있다면 제안 부탁드립니다.
마지막으로, 클래스 내부에 프로토콜이 존재할 수 없기 때문에 Pizza의 abstract builder는 클래스 외부에 선언하였습니다.
protocol DefaultBuilder: class {
associatedtype BuilderType: DefaultBuilder
var toppings: Set<Pizza.Topping> { get set }
// swift는 covariant return typing을 지원하지 않는 것 같아서 여기서 정의하지 않고,
// 각 구체 타입에 구현하였습니다.
// 더 좋은 방법이 있다면 제안 부탁드립니다.
//func build() -> Pizza
}
extension DefaultBuilder {
func addTopping(_ topping: Pizza.Topping) -> Self { // 구체 하위 빌더 반환(책에서 T에 해당)
toppings.insert(topping)
return self
}
}
class Pizza {
enum Topping {
case ham, mushroom, onion, pepper, sausage
}
let toppings: Set<Topping>
init<Builder: DefaultBuilder>(with builder: Builder) {
// Set은 value type이기 때문에 clone하지 않아도 복사본이 할당됩니다.
toppings = builder.toppings
}
}
다음은 Pizza를 상속한 두 구체 타입들입니다.
class NewYorkPizza: Pizza {
enum Size {
case small, medium, large
}
let size: Size
class Builder: DefaultBuilder {
typealias BuilderType = Builder
var toppings = Set<Pizza.Topping>()
let size: Size
init(size: Size) {
self.size = size
}
func build() -> NewYorkPizza {
return NewYorkPizza(builder: self)
}
}
init(builder: Builder) {
size = builder.size
super.init(with: builder)
}
}
class Calzone: Pizza {
let sauceInside: Bool
class Builder: DefaultBuilder {
typealias BuilderType = Builder
var toppings = Set<Pizza.Topping>()
var sauceInside = false
func addSauce() -> Self { // 변수명과 겹쳐서 addSauce로 변경
sauceInside = true
return self
}
func build() -> Calzone {
return Calzone(builder: self)
}
}
init(builder: Builder) {
sauceInside = builder.sauceInside
super.init(with: builder)
}
}
다음은 위 계층적 빌더들을 사용하는 클라이언트 코드의 예시입니다. build()에서 반환하는 타입을 명확히 하기 위해 변수의 타입을 명시하였습니다.
let newYorkpizza: NewYorkPizza = NewYorkPizza.Builder(size: .large)
.addTopping(.ham).addTopping(.mushroom).build()
let calzone: Calzone = Calzone.Builder()
.addSauce().addTopping(.sausage).build()
생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫습니다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇습니다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전합니다.
선택적 프로퍼티가 많은 URLComponent에도 빌더 패턴을 활용할 수 있습니다.