How to use Builder Design Pattern In Swift

Subscribe to my newsletter and never miss my upcoming articles

As a developer, we should always follow a design principle that acts as a guide to structuring our code so that it is modular, easy to read, easy to understand, and scalable. In this article, I will be discussing behavioral design pattern, how to use them, and their implementation in Swift. Read on!

Categories of Desing Pattern Design Pattern falls mainly under the following categories

1. Creational

2. Structural

3. Behavioral

In this article, we will cover the Builder Designer Pattern which is a type of Creational Design Pattern

Builder Design Pattern

Why?

We create objects for our classes to leverage the functionality a class provides. Sometimes object creation is simple and can be done by the simple initializer. Other objects might have complicated requirements, for eg, it may require a lot of arguments to initialize the object, which in my opinion is too cumbersome and non-productive. Also, we might need to mix and match these params for initialization. In these cases, we should go for piecewise initialization/construction. For accomplishing that we need an implementation that provides us a step-by-step mechanism so that we have an easier API way of accessing things with granular control of object creation version.

Let's take an example to understand. Say we want to make an object APIRequest class for our recipe app. This class helps us create request objects to make different API requests. For starters, the current API request class helps us create objects with only two params.

enum Endpoint:String {

    case receipesUrl = "/recipes"

    case receipeDetail = "/recipes/id"

}

class ApiRequest {

    var endpoint:Endpoint

    init(endPoint:Endpoint) {

        self.endpoint = endPoint

    }

}

var apiRequest = ApiRequest(endPoint: .receipesUrl)

Now say you want to pass it in the Http method (POST, GET, etc), headers, URL params, etc. So let's say we add more parameters to our initializer which may look like this now.

enum HTTPMethod:String {

    case get = "GET"

    case post = "POST"

}

enum Endpoint:String {

    case receipesUrl = "/recepies"

    case receipeDetail = "/recepies/id"

}

class ApiRequest {

    var endpoint:Endpoint

    var urlParams:[String:String]

    var httpMethod:HTTPMethod

    var headers:[String:String]

    init(endPoint:Endpoint, httpMethod:HTTPMethod, headers:[String:String], urlParams:[String:String]) {

        self.endpoint = endPoint

        self.httpMethod = httpMethod

        self.headers = headers

        self.urlParams = urlParams

    }

}

var apiRequest = ApiRequest(endPoint: .receipesUrl, httpMethod: .get, headers: [:],urlParams: [:])

As you can see now our initializer has started to grow up. We still need to add a bunch of more parameters like search params, HTTP scheme(HTTP or HTTPS), path parameters, payload, filters, etc. With so many parameters this will become very ugly, plus we then have to handle passing the different params for the initialization which we might not need. We could have a default value or make some of the params optional but still, it is not ideal.

Solution

So in this kind of situation, the builder design pattern really shines. The Builder design pattern is useful to create objects that require various configuration options. It not only gives us a nice API way but also gives us control as to what flavor of our objection creation we want. So basically we will have a function to pass different params via function and every function will return the ApiRequestBuilder type.

Example Now let implement the above example using the builder pattern

class ApiRequestBuilder {

    var httpMethod:HTTPMethod

    var endpoint: Endpoint

    var urlParams:[String:String]?

    var headers:[String:String]?

    init(endpoint:Endpoint, httpMethod:HTTPMethod) {

        self.endpoint = endpoint

        self.httpMethod = httpMethod

    }

    func urlParams(urlParams:[String:String]?) -> ApiRequestBuilder {

        self.urlParams = urlParams

        return self

    }

    func headers(headers:[String:String]?) -> ApiRequestBuilder {

        self.headers = headers

        return self

    }

}

let apiRequest = ApiRequestBuilder(endpoint: .receipesUrl, httpMethod: .get)

    .headers(headers: ["clientId":"xyz"])

    .urlParams(urlParams: ["id":"abc"])

Say now if we want to add support for additional params we can easily expand our api. We will now add support for passing searchBy, payload, filters etc.

class ApiRequestBuilder {

    var httpMethod:HTTPMethod

    var endpoint:Endpoint

    var urlParams:[String:String]?

    var headers:[String:String]?

    var payload:[String:Any]?

    var filters:[String:String]?

    var searchBy:[String:String]?

    init(endpoint:Endpoint, httpMethod:HTTPMethod) {

        self.endpoint = endpoint

        self.httpMethod = httpMethod

    }

    func urlParams(urlParams:[String:String]?) -> ApiRequestBuilder {

        self.urlParams = urlParams

        return self

    }

    func headers(headers:[String:String]) -> ApiRequestBuilder {

        self.headers = headers

        return self

    }

    func payload(payload:[String:Any]) ->ApiRequestBuilder {

        self.payload = payload

        return self

    }

    func filters(filters:[String:String]) ->ApiRequestBuilder {

        self.filters = filters

        return self

    }

    func searchBy(searchBy:[String:String]) ->ApiRequestBuilder {

        self.searchBy = searchBy

        return self

    }
}

Now let's say we want to make a request to GET a list of recipes. We will initiate our request in this simple way

let apiRequest = ApiRequestBuilder(endpoint: .receipesUrl, httpMethod: .get)

    .headers(headers: ["clientId":"xyz"])

    .urlParams(urlParams: ["id":"abc"])

To get the result refined let's add filters and searchBy to this

let apiRequest = ApiRequestBuilder(endpoint: .receipesUrl, httpMethod: .get)

    .headers(headers: ["clientId":"xyz"])

    .urlParams(urlParams: ["id":"abc"])

    .filters(filters: ["createdTime >=":"1601261533"])

    .searchBy(searchBy: ["name":"pasta"])

So see how conveniently we were able to expand our objection creation in a nice api form which is easier to understand and consume.

Conclusion

Builder is a creational design pattern that allows the creation of an object is a step by step fashion. It is useful to create objects that require various configuration options.

No Comments Yet