본문 바로가기

Architecture & Design Pattern

[Design Pattern] 빌더 패턴 (Creational Pattern - 1)

생성 패턴(Creational Pattern)

생성패턴은 객체의 생성에 관련된 패턴으로, 객체의 생성 절차를 추상화하는 패턴이다.

객체를 생성하고 합성하는 방법 / 객체의 표현방법과 시스템을 분리한다.

 

🐣 디자인 패턴은 어떠한 문제가 발생할 때, 효과적으로 해결하기 위해서 만든 패턴. 많은 개발자들이 쌓아온 솔루션과 같은 것이다. 생성 패턴은 어떤 문제를 해결할 수 있을까?

특징

객체의 생성과 조합을 캡슐화해서, 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공한다.

따라서, 특정 객체를 추가하거나 변경을 하는 비용을 줄이도록 하는 방법이다.

종류

추상 팩토리 패턴

동일한 주제의 다른 팩토리를 묶어준다

빌더 패턴

생성과 표기를 분리해 복잡한 객체를 생성한다

팩토리 메서드 패턴

생성할 객체의 클래스를 국한하지 않고 객체를 생성한다

프로토타입 패턴

기존 객체를 복제함으로써 객체를 생성한다

싱글턴 패턴

한 클래스에 한 객체만 존재하도록 제한한다.

빌더 패턴(Builder Pattern)

복잡한 객체를 생성할 때, 생성자를 이용해서 한 번에 구현하지 않고 속성을 분리하여 서로 다른 표현형을 가진 객체를 만드는 패턴이다.

언제 사용하나요?

객체의 표현을 단계에 따라 세밀하게 조정하고 싶을 때.

객체의 프로퍼티가 너무 많아서 객체를 구성하는 코드를 분리하고 싶을 때.

빌더 패턴의 요소

Director

input을 받아서 이를 builder와 조정한다. iOS에서 VC와 같은 것.

빌더 패턴의 설계 방식에 따라 Director를 이용해서 builder에 접근할 수도 있고, builder를 바로 이용하여 product를 생성할 수도 있다.

Builder

단계별 입력을 받아서 객체를 구성한다. 클래스로 만들어서 참조할 수도 있다.

Product

결과물로 생기는 복잡한 객체.

주의점

간단한 객체를 빌더 패턴으로 만들면 오히려 더 복잡해지고 비효율적인 방식이 된다.

빌더 패턴의 종류

BlockBased Builder

BlockBase Builder는 Swift의 Extension이라는 기능을 이용하여 구현할 수 있는 빌더 패턴의 한 종류이다.

import UIKit

extension UIButton {
    static func build(block: ((UIButton) -> Void)) -> UIButton {
        let button = UIButton()
        block(button)

        return button
    }
}

아래처럼 따로 기존 클로저를 사용한 방법과 다르게 return문을 사용해줄 필요가 없다.

private lazy var signUpButton = UIButton.build { button in
    button.translatesAutoresizingMaskIntoConstraints = false
    button.setBackgroundImage(UIImage(color: .rgb(red: 0.0, green: 117.0, blue: 255.0)), for: .normal)
    button.addTarget(self, action: #selector(touchUpInside(signUpButton:)), for: .touchUpInside)
    button.titleLabel?.font = .systemFont(ofSize: 18.0, weight: .semibold)
    button.setTitleColor(.white, for: .normal)
    button.setTitle("Sign Up", for: .normal)
    button.layer.cornerRadius = 5.0
    button.clipsToBounds = true
}

Simple Builder

Simple Builder는 프로퍼티를 나눠서 설정해준다기 보다는, 객체 생성에 대한 정보를 가지고 있는 빌더 클래스이다.

아래는 빌더가 만들어줄 모델의 구조체이다.

import UIKit

struct Content {
    let title: String
    let image: UIImage?
    let text: String
    let textColor: UIColor
    let textWeight: UIFont.Weight
    let textAlignment: NSTextAlignment
    let backgroundColor: UIColor
}

단순하게 bulder 클래스가 genre에 대한 열거형을 가지고 있고, 이 열거형의 케이스에 따라서 build() 메서드가 적절한 content를 리턴해준다.

import UIKit

class ContentBuilder {
    enum Genre {
        case movie
        case music
        case gaming
        case sports
    }

    func build(for genre: Genre) -> Content {
        switch genre {
        case .movie:
            return Content(
                title: "Movie Content",
                image: UIImage(named: "ic-movie"),
                text: "This is an example of Content screen made with the help of Content Builder",
                textColor: .rgb(red: 120.0, green: 115.0, blue: 110.0),
                textWeight: .regular,
                textAlignment: .center,
                backgroundColor: .rgb(red: 240.0, green: 235.0, blue: 230.0)
            )
        case .music:
            return Content(
                title: "Music Content",
                image: UIImage(named: "ic-music"),
                text: "This content screen data object is built with the help of Builder Pattern",
                textColor: .rgb(red: 120.0, green: 130.0, blue: 200.0),
                textWeight: .semibold,
                textAlignment: .center,
                backgroundColor: .rgb(red: 210.0, green: 240.0, blue: 250.0)
            )
        case .gaming:
            return Content(
                title: "Gaming Content",
                image: UIImage(named: "ic-gaming"),
                text: "Content object for this screen was made via Simple Builder Pattern",
                textColor: .rgb(value: 50.0),
                textWeight: .thin,
                textAlignment: .center,
                backgroundColor: .rgb(value: 220.0)
            )
        case .sports:
            return Content(
                title: "Sports Content",
                image: UIImage(named: "ic-sports"),
                text: "Content in this screen is a result of build function in Builder Class",
                textColor: .black,
                textWeight: .regular,
                textAlignment: .center,
                backgroundColor: .white
            )
        }
    }
}

테이블뷰의 각 셀을 터치했을 때, 적절한 content를 가진 viewController를 띄워야 하는데, builder를 이용해 굉장히 깔끔하게 구현한 것을 확인 가능하다.

extension MainViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)

        switch indexPath.section {
        case 0:
            let builder = ContentBuilder()
            let content: Content

            if indexPath.row == 0 {
                content = builder.build(for: .movie)
            } else if indexPath.row == 1 {
                content = builder.build(for: .music)
            } else if indexPath.row == 2 {
                content = builder.build(for: .gaming)
            } else {
                content = builder.build(for: .sports)
            }

            let viewController = ContentViewController(content: content)
            navigationController?.pushViewController(viewController, animated: true)
				}
		}
}

Chained Builder

빌더 클래스는 product를 private하게 가지고 있고, 프로퍼티의 설정은 메서드를 통해서만 가능하다.

각 메서드는 다시 Builder를 반환해서, Builder가 가진 메서드에 접근 가능하도록 한다.

최종적으로 build()메서드를 실행하면 product가 반환된다.

import UIKit

class UITextFieldBuilder {
    private var textField: UITextField

    // MARK: - Init

    init() {
        textField = UITextField()
    }
}

// MARK: - Actions

extension UITextFieldBuilder {
    func setText(_ text: String) -> UITextFieldBuilder {
        textField.text = text
        return self
    }

    func setPlaceholder(_ placeholder: String) -> UITextFieldBuilder {
        textField.placeholder = placeholder
        return self
    }

    func setFont(_ font: UIFont) -> UITextFieldBuilder {
        textField.font = font
        return self
    }

    func setTextAlignment(_ alignment: NSTextAlignment) -> UITextFieldBuilder {
        textField.textAlignment = alignment
        return self
    }

    func setDelegate(_ delegate: UITextFieldDelegate) -> UITextFieldBuilder {
        textField.delegate = delegate
        return self
    }

    func setKeyboardType(_ keyboardType: UIKeyboardType) -> UITextFieldBuilder {
        textField.keyboardType = keyboardType
        return self
    }

    func setReturnKeyType(_ returnKeyType: UIReturnKeyType) -> UITextFieldBuilder {
        textField.returnKeyType = returnKeyType
        return self
    }

    func setAutocorrectionType(_ autocorrectionType: UITextAutocorrectionType) -> UITextFieldBuilder {
        textField.autocorrectionType = autocorrectionType
        return self
    }

    func setAutocapitalizationType(_ autocapitalizationType: UITextAutocapitalizationType) -> UITextFieldBuilder {
        textField.autocapitalizationType = autocapitalizationType
        return self
    }

    func setClearButtonMode(_ clearButtonMode: UITextField.ViewMode) -> UITextFieldBuilder {
        textField.clearButtonMode = clearButtonMode
        return self
    }

    func setIsSecureTextEntry(_ isSecureTextEntry: Bool) -> UITextFieldBuilder {
        textField.isSecureTextEntry = isSecureTextEntry
        return self
    }

    func build() -> UITextField { textField }
}
private lazy var defaultTextField: UITextField = {
    UITextFieldBuilder()
        .setPlaceholder("Default TextField")
        .setFont(.systemFont(ofSize: 17.0))
        .setClearButtonMode(.never)
        .setReturnKeyType(.next)
        .setAutocorrectionType(.no)
        .setAutocapitalizationType(.words)
        .setDelegate(self)
        .build()
}()

Complex Builder

BuilderError

Builder를 통한 객체 생성 시에 생길 수 있는 Error 타입을 정의한다.

  • BuilderError 코드
import Foundation

protocol BuildableModel { }

enum BuilderError<Field: BuilderField>: Error, CustomStringConvertible {
    case missingInfo(Field)
    case invalidInfo(Field)
    case tooShortValue(Field, Int)
    case failedMatching(Field)

    var description: String {
        switch self {
        case .missingInfo(let field):
            return "\(field.title) cannot be empty.\nPlease enter your \(field.title)"
        case .invalidInfo(let field):
            return "Please enter a valid \(field.title)."
        case .tooShortValue(let field, let requiredCharacters):
            return "\(field.title) is too short. It should have at least \(requiredCharacters) characters."
        case .failedMatching(let field):
            return "\(field.title) does not match."
        }
    }
}

Builder 프로토콜과 SignUpBuidler 클래스

Builder 프로토콜

builder 프로토콜을 이용해서 Builder 클래스들이 어떤 프로퍼티를 가져야할 지 정의하고 있다.

feilds는 Set(집합)으로 정의해서, set()메서드를 통해 update해주고 있다.
여기서 field는 singUpField의 열거형이고, signUpField는 textField의 text를 연관값으로 가진다.

 

associatedType은 프로토콜이 채택되기 전까지 구체 타입이 명시되지 않는다. 따라서 채택하는 클래스에 따라 구현 시점에 타입을 정해줄 수 있기에 프로그래밍에 유연성을 부여한다.

 

SignUpBuilder 클래스

SingUpBuilder 클래스는 Builder 프로토콜을 채택하는데, 아까 정의해둔 연관타입을 구체적으로 정해주고 있고, build 시에 실행할 로직을 구현해주고 있다. validateAll을 통해 각각의 필드들이 유효한지 검사하고, 비밀번호가 올바르게 매칭되는지 검사한다.

import Foundation

protocol Builder: class {
    associatedtype FieldType: BuilderField
    associatedtype Model: BuildableModel

    var fields: Set<FieldType> { get set }

    func build() throws -> Model
}

extension Builder {
    func set(field: FieldType) -> Self {
        fields.update(with: field)
        return self
    }

    func getField(with identifier: String) -> FieldType? {
        guard let index = fields.firstIndex(where: { $0.identifier == identifier } ) else { return nil }
        return fields[index]
    }

    func validateAll() throws {
        for field in fields {
            try field.validate()
        }
    }
}

class SignUpModelBuilder: Builder {
    typealias FieldType = SignUpField
    typealias Model = SignUpModel

    var fields: Set<SignUpField> = []

    func matchPasswords() throws {
        if let password = getField(with: SignUpField.password(nil).identifier),
            let confirmPassword = getField(with: SignUpField.confirmPassword(nil).identifier),
            password.value != confirmPassword.value {
            throw BuilderError.failedMatching(confirmPassword)
        }
    }

    func build() throws -> SignUpModel {
        try validateAll()
        try matchPasswords()

        let name = getField(with: SignUpField.name(nil).identifier)?.value
        let email = getField(with: SignUpField.email(nil).identifier)?.value
        let phone = getField(with: SignUpField.phone(nil).identifier)?.value
        let password = getField(with: SignUpField.password(nil).identifier)?.value


        return SignUpModel(name: name, email: email, phone: phone, password: password)
    }
}

ViewController에서의 사용 예시

createSignUpModel 메서드는 버튼이 눌릴 때마다 동작한다.

try ~ catch로 빌더를 사용해서 객체를 build할 때 에러가 난다면 에러 발생을 잡아내는 구조이다.

.set 메서드는 builder 프로토콜에 초기구현되어 있다.

private func createSignUpModel() {
    do {
        let signUpModel = try SignUpModelBuilder()
            .set(field: .name(nameTextField.text))
            .set(field: .email(emailTextField.text))
            .set(field: .phone(phoneTextField.text))
            .set(field: .password(passwordTextField.text))
            .set(field: .confirmPassword(confirmPasswordTextField.text))
            .build()

        let message = """
        The following Sign Up Model was created using Builder Pattern
        name: \\(signUpModel.name ?? ""))
        email: \\(signUpModel.email ?? ""))
        phone: \\(signUpModel.phone ?? ""))
        password: \\(signUpModel.password ?? ""))
        """
        showAlertController(with: "Success", message: message)
    } catch let error {
        showAlertController(with: "Error", message: "\\(error)")
    }
}

참조

[Swift 디자인 패턴] Builder Pattern (빌더) - 디자인 패턴 공부 3

스위프트에서 빌더 패턴 구현

https://github.com/tagaruma/builder-design-pattern