Using the builder pattern in Swift

Like many abstractions and patterns in programming, the goal of the builder pattern is to reduce the need to keep mutable state - resulting in objects that are simpler and generally more predictable. By enabling objects to become stateless, they are usually much easier to test and debug - since their logic consists only of "pure" input & output.

While the builder pattern is very common in languages like Java, it isn't something you commonly encounter when working on Apple's platforms (at least not as explicit builder objects). This week, let's take a look at how the builder pattern works, what problems it aims to solve, and how it can be used in various situations in Swift.

Building objects

The core idea behind the builder pattern is that the process of setting up an object is performed by a dedicated Builder type, rather than by the object itself. As an example, let's say we wanted to setup an ArticleView with a title, a subtitle and an image. A common way to do that would be to expose the subviews that will render the content and set properties on them, like this:

let view = ArticleView()
view.titleLabel.text = article.title
view.subtitleLabel.text = article.subtitle
view.imageView.image = article.image

Let's now try to use the builder pattern instead. Instead of accessing the subviews of ArticleView, we'll use an ArticleViewBuilder to set all the properties we need through a series of chained method calls, and then we'll finish by calling build() to create an instance - like this:

let view = ArticleViewBuilder()
    .withTitle(article.title)
    .withSubtitle(article.subtitle)
    .withImage(article.image)
    .build()

As you can see above, a common practice is to have the builder return itself from each method call, making it easy to chain setup commands without having to introduce additional local variables.

Separated mutability

While the builder pattern does introduce extra code (since we now also have to create a builder type), it has two major strengths.

First; it lets us close down a lot of public, mutable APIs that we otherwise have to keep available for configuration. After introducing a builder in our ArticleView example above, we can now make titleLabel, subtitleLabel and imageView private (since all configuration is now done through ArticleViewBuilder) - leaving us with a much more simple view that doesn't have to react to changes in content.

Second; it prevents mutable state from being accidentally shared. Historically, Apple have used pairs of mutable & immutable objects when designing their frameworks. For example, when working with attributed strings we use NSAttributedString for immutable values and NSMutableAttributedString for mutable values. While this is mostly because of Objective-C's lack of Swift-like value types (since NSAttributedString doesn't have a Swift equivalent yet), it's still something that can cause bugs in our Swift code.

For example, let's say we're building an app that lets the user input letters by drawing on the screen, and that we use an NSMutableAttributedString to keep track of the total text that the user has entered. Then, when a "Save" button is tapped, we save the attributed string to our database, like this:

class TextDrawingViewController: UIViewController {
    private let text: NSMutableAttributedString

    private func appendCharacter(_ character: Character) {
        let string = NSAttributedString(
            string: String(character),
            attributes: textAttributes
        )

        text.append(string)
    }

    private func handleSaveButtonTap() {
        // Our database accepts an 'NSAttributedString', but since 
        // 'NSMutableAttributedString' inherits from its immutable
        // counterpart, we're able to pass 'text' directly here.
        database.save(text) {
            ...
        }
    }
}

Note above how we are able to pass an NSMutableAttributedString to an API that accepts an NSAttributedString, since the former is a subclass of the latter. While this may seem harmless, it's a common source of tricky bugs since mutable state ends up being shared between TextDrawingViewController and our database. If the user enters another character while the database is busy saving, we might end up in an undefined state (or crash), since the database isn't expecting the passed text to be mutated while it's working on it.

If we instead were to use the builder pattern, it would be impossible to accidentally pass a mutable object as an immutable one - since they would be distinct, unrelated types. To do that, let's introduce a simple AttributedStringBuilder that we can use to append characters:

class AttributedStringBuilder {
    typealias Attributes = [NSAttributedStringKey : Any]

    private let string = NSMutableAttributedString(string: "")

    // We follow the convention of returning the builder object
    // itself from any configuration methods, and by adding the
    // @discardableResult attribute we won't get warnings if we
    // don't end up doing any chaining.
    @discardableResult
    func append(_ character: Character,
                attributes: Attributes) -> AttributedStringBuilder {
        let addedString = NSAttributedString(
            string: String(character),
            attributes: attributes
        )

        string.append(addedString)

        return self
    }

    func build() -> NSAttributedString {
        return NSAttributedString(attributedString: string)
    }
}

With the above in place, let's now update our view controller to use our new builder, instead of working on an NSMutableAttributedString directly:

class TextDrawingViewController: UIViewController {
    private let textBuilder = AttributedStringBuilder()

    private func appendCharacter(_ character: Character) {
        textBuilder.append(character, attributes: textAttributes)
    }

    private func handleSaveButtonTap() {
        let text = textBuilder.build()

        database.save(text) {
            ...
        }
    }
}

We have now removed the risk of accidentally sharing mutable state, since we have to call build() on our builder to create an actual attributed string that can be passed to our database ๐Ÿ‘.

Hiding complexity

The builder pattern can also be really useful in order to provide a simple API for a complex task. Above, we used a simple database API that let us save an NSAttributedString using a single method call, but many apps require much more powerful and complex database features. Let's take a look at how the builder pattern can be used to hide implementation details, allowing us to make our database API a lot more powerful while still maintaining ease of use.

The next version of our database introduces types like Query<Record> and QueryOperation<Record> that are used internally to filter, match and limit the records that we're interested in. However, instead of exposing all of those details to the API user, let's create a QueryBuilder that lets us construct a query step-by-step:

class QueryBuilder<Record> {
    // By using a builder, we're able to hide the QueryOperation type completely,
    // while also isolating mutability.
    private var operations = [QueryOperation<Record>]()

    @discardableResult
    func matchRecords(with string: String) -> QueryBuilder<Record> {
        operations.append(.match(string))
        return self
    }

    @discardableResult
    func limitNumberOfResults(to count: Int) -> QueryBuilder<Record> {
        operations.append(.addLimit(count))
        return self
    }

    @discardableResult
    func filterByKeyPath<T: Equatable>(_ keyPath: KeyPath<Record, T>,
                                       value: T) -> QueryBuilder<Record> {
        operations.append(.filter { record in
            return record[keyPath: keyPath] == value
        })

        return self
    }

    func build() -> Query<Record> {
        return Query(operations: operations)
    }
}

With the above in place, we can now write even more complex database code in a much simpler, declarative fashion. Here's how we could use our new API to find all Article records that the user have added to their favorites:

func findFavorites(matching string: String) -> [Article] {
    let query = QueryBuilder<Article>()
        .matchRecords(with: searchString)
        .limitNumberOfResults(to: 10)
        .filterByKeyPath(\.isFavorite, value: true)
        .build()

    return database.records(matching: query)
}

Note how we use Swift's powerful key paths feature above to provide a simple, yet type safe, way of filtering records based on the value of a property. We'll dive deeper into key paths in an upcoming post.

Conclusion

While structs and value types have removed lots of the use cases for the builder pattern in Swift, it's still a nice tool to keep in our toolbox for certain types of situations - when using a value type is either unpractical or impossible (like when dealing with Objective-C APIs). The builder pattern can also really come in handy when creating public APIs and frameworks, since it enables easy customization without having to introduce mutability or expose implementation details.

I mentioned in the intro that Apple haven't made much use of the builder pattern in their frameworks and SDKs. While it's true that they haven't (to my knowledge) used explicit Builder objects, there are certain aspects of some APIs that do follow this kind of convention. Take URLComponents and DateComponents for example, which can both kind of be seen as builder-like equivalents of URL and Date (both of which don't have the Mutable counterparts like so many other Foundation objects do).

What do you think? Is the builder pattern something you've used before, or is it something you'll try out? Let me know, along with any questions, comments or feedback - on Twitter @johnsundell.

Thanks for reading! ๐Ÿš€

Observers in Swift - Part 1

Conditional conformances in Swift