Skip to content

Builder

Jakub Vano edited this page Jun 27, 2017 · 4 revisions

Overview

Builder pattern is commonly used when creating/modifying domain model via form. Typically flow looks like this:

  1. View passes user-entered raw values (Strings) to Presenter
  2. Presenter converts raw data to partial models (i.e. String -> Double) via Parsers, Presenter also handles empty strings.
  3. Presenter passes partial models to Builder
  4. View invokes some sort of submit action on Presenter
  5. Presenter calls Builder.build()
  6. Builder constructs final domain model, while performing necessary validations, and returns either model or validation error / errors.
  7. Presenter handles the results.

Example:

Models and interfaces:

enum PaymentOrderError {
    case invalidAmount
    case missingAmount
    case invalidRecipient
    case missingRecipient
}

struct PaymentOrder {
    let amount: Money
    let recipient: Iban
    let note: String?
}

protocol PaymentOrderBuilder {
    var amount: Parsed<Double> { get set }
    var recipient: String? { get set }
    var note: String? { get set }
    func build() -> Result<PaymentOrder, CompositeError<PaymentOrderError>>
}

protocol PaymentOrderPresenter {
    func updateAmount(_ amount: String)
    func updateRecipient(_ recipient: String)
    func updateNote(_ note: String)
    func submit()
}

Implementations:

final class PaymentOrderPresenterImpl: PaymentOrderPresenter {
    private let builder: PaymentOrderBuilder
    private let doubleParser: StringToDoubleParser

    init(builder: PaymentOrderBuilder, doubleParser: StringToDoubleParser) {
        self.builder = builder
        self.doubleParser = doubleParser
    }

    func updateAmount(_ amount: String) {
        builder.amount = doubleParser.parse(amount)
    }

    func updateRecipient(_ recipient: String) {
        builder.recipient = recipient.nilIfEmpty
    }

    func updateNote(_ note: String) {
        builder.note = note.nilIfEmpty
    }

    func submit() {
        switch builder.build() {
        case .success(let paymentOrder):
            handlePaymentOrder(paymentOrder)
        case .failure(let error):
            handleError(error)
        }
    }

    private func handlePaymentOrder(_ paymentOrder: PaymentOrder) {

    }

    private func handleError(_ error: CompositeError<PaymentOrderError>) {

    }
}

final class PaymentOrderBuilderImpl: PaymentOrderBuilder {
    var amount: Parsed<Double> = .missing
    var recipient: String?
    var note: String?

    private let ibanValidator: IbanValidator

    init(ibanValidator: IbanValidator) {
        self.ibanValidator = ibanValidator
    }

    func build() -> Result<PaymentOrder, CompositeError<PaymentOrderError>>{
        return buildAmount().mapError(CompositeError.init)
            .pack(buildRecipient().mapError(CompositeError.init))
            .pack(Result.success(note))
            .map(flatten)
            .map(PaymentOrder.init)
    }

    private func buildAmount() -> Result<Money, PaymentOrderError> {
        switch amount {
        case .missing:
            return .failure(.missingAmount)
        case .invalid:
            return .failure(.invalidAmount)
        case .value(let amount):
            return .success(Money(amount: amount, currency: .EUR))
        }
    }

    private func buildRecipient() -> Result<Iban, PaymentOrderError> {
        guard let recipient = recipient else {
            return .failure(.missingRecipient)
        }
        guard ibanValidator.isValid(recipient) else {
            return .failure(.invalidRecipient)
        }
        return .success(Iban(recipient))
    }
}
Clone this wiki locally