Cocoa at Tumblr — Functions As Factories

1.5M ratings
277k ratings

See, that’s what the app is perfect for.

Sounds perfect Wahhhh, I don’t wanna

Functions As Factories

Factories are a fairly well understood design pattern in software development.  The benefits of using factories include:

  1. Abstracting constructors away from clients.
  2. Encapsulating data that clients do not need to know about.
  3. Allowing for more testable code by enforcing the idea of passing objects into initializers instead of referencing singletons directly.

This post will show the power of a few Swift features as well as of first­-class functions.

Consider a setup like this in a piece of software that needs to send network requests:

struct APIURL {
    let APICreds: APICredentials
    let baseURL: String
    let route: String
    let queryParams: [String: String]
    
    func URLString() -> String {
        func finalQueryParams() -> [String : String] {
           return .....
        }
        return encodedURLWithQueryParams(baseURL + route, params: finalQueryParams())
    }
    
    private func encodedURLWithQueryParams(URL: String, params: [String: String]) -> String{
        return ....
    }
}
struct APINetworkRequest {
    let userCredentials: UserCredentials
    let URL: APIURL
    
    private func sign() {
        // Sign the request using the user’s credentials
    }
}

Many parts of your application will need to create requests for different routes, and as such will need to create new APINetworkRequest instances (and therefore, new APIURLs). In order to do this, these parts of your application will need to reference the API credentials, the application API credentials, the base URL, and query parameters. We can pass these items from wherever we first create them (maybe in application(application:, didFinishLaunchingWithOptions:) or after a login screen) to whichever objects need them, or we can create globally accessible singletons that other parts of our codebase can access. 

Unfortunately, these solutions are both sub par; passing this many values throughout our codebase via initializers is cumbersome, and spreading knowledge of singletons throughout the application is to tightly couple, making testing very difficult.

A solution to this could be a factory that can construct an APINetworkRequest for each route, which might look something like this:

struct APINetworkRequestFactory {
    let userCredentials: UserCredentials
    let APICreds: APICredentials
    let baseURL: String
    
    func secretAdminInfo(adminId: String) -> APINetworkRequest {
        return APINetworkRequest(userCredentials: userCredentials,
            URL: APIURL(APICreds: APICreds, baseURL: baseURL, route: "sekret/info/admin", queryParams: ["id" : adminId]))
    }
    
    func search(query: String) -> APINetworkRequest {
        return APINetworkRequest(userCredentials: userCredentials,
            URL: APIURL(APICreds: APICreds, baseURL: baseURL, route: "search", queryParams: ["query" : query]))
    }
    
    func translate(term: String, toLanguage: String) -> APIURL {
        return APIURL(APICreds: APICreds, baseURL: baseURL, route: "translate", queryParams: ["term" : term, "to_language" : toLanguage])
    }
    
    func allData() -> APIURL {
        return APIURL(APICreds: APICreds, baseURL: baseURL, route: "alldata", queryParams: [String: String]())
    }
}

Instead of passing four or more values through each initializer, we can just pass around this factory and use it to conveniently construct APINetworkRequests for any route.

Could we make this even more painless, however? Can we achieve the same benefit by using a function as our “factory”? We can!

Let’s see what a function that can create all of these routes might look like. First, let’s create an enum to denote which route we want to construct a request for:

enum Route {
    case SecretAdminInfo
    case Search
    case Translate
    case AllData
}

Now, we can create a function that returns an APINetworkRequest, with a signature that looks something like this:

func APINetworkFactoryFunction(APICreds: APICredentials, userCredentials: UserCredentials, baseURL: String, route: Route) -> APINetworkRequest

Since Swift has first class functions, this function can be passed as a method argument in the same way that we would’ve otherwise passed an instance of our custom factory class.

Inside this APINetworkFactoryFunction function we can simply use a switch statement to switch on the route argument to construct an APINetworkRequest for each API route just like the APINetworkRequestFactory did.

There is one problem, however, as you can tell by looking at different method signatures on APINetworkRequestFactory: different routes take different arguments. This makes sense since they are used to perform very different tasks and, as such, require different inputs. With this enum in its current state, we cannot create the variety of fully constructed network requests that our application needs.

One solution to this is to use a feature of Swift Enumerations called Associated Values. We can re-declare our Route enum to look like this:

enum Route {
    case SecretAdminInfo(adminId: String)
    case Search(query: String)
    case Translate(term: String, toLanguage: String)
    case AllData()
}

Now, the switch statement inside of our APINetworkFactoryFunction can use these values in order to create our APINetworkRequests. The APINetworkFactoryFunction implementation could look something like this:

func APINetworkFactoryFunction(APICreds: APICredentials, userCredentials: UserCredentials, baseURL: String, route: Route) -> APINetworkRequest {
    switch route {
    case let .SecretAdminInfo(adminId):
        // Create and return an APINetworkRequest using adminId
    case let .Search(query):
        // Create and return an APINetworkRequest using query
    case let .Translate(term, toLanguage):
        // Create and return an APINetworkRequest using term and toLanguage
    case let .AllData():
        // Create and return an APINetworkRequest
    }
}

So now we can create the proper APINetworkRequests just like our original APINetworkRequestFactory did. However, invoking this function requires having all of it’s arguments at the call site. Our goal with introducing the factory in the first place was to avoid passing these arguments throughout the codebase. We really want to pass a function around that takes only a Route and returns a APINetworkRequest that already knows around all the other arguments. This is a great example of where function currying can be used. Swift provides special syntax for creating curried functions, which we will use.

So we can turn the function signature we had before into this:

func APINeworkFactoryFunction(APICreds: APICredentials, userCredentials: UserCredentials, baseURL: String)(route: Route) -> APINetworkRequest

Notice how the last argument is in its own set of parenthesis. APINetworkFactoryFunction’s type is now (APICredentials, UserCredentials, String) -> (route: Route) -> APINetworkRequest – a function that returns a function that returns an APINetworkRequest.

For example, if we wanted only a function that takes a Route (a function of type (route: Route) -> APINetworkRequest) we can create one by currying our original function:

let functionToPassToClients = APINeworkFactoryFunction(APICredentials(key: "key", secret: "sekret"), UserCredentials(token: "token", tokenSecret: "tokenSekret"), "https://tumblr.com")

This is very powerful, as the function now provides exactly the same functionality as the APINetworkRequestFactory. But can this be even more flexible?

Let’s say you don’t yet have all of the inputs needed to create a new Route instance; you can instead create just a function that takes a Route in parts. For example you might have the application credentials at launch but you don’t have the user credentials until after login. We can solve this by further currying our function so that you pass arguments in one at a time, like so:

func APINeworkFactoryFunction(APICreds: APICredentials)(userCredentials: UserCredentials)(baseURL: String)(route: Route) -> APINetworkRequest

You can set the API credentials first:

let factoryFunction = APINeworkFactoryFunction(APICredentials(key: "key", secret: "sekret"))

and then when you get the user’s credentials you can “add” them in:

let factoryFunctionIncludingUserCredentials = factoryFunction(userCredentials: UserCredentials(token: "token", tokenSecret: "tokenSekret"))

Finally, when you’re ready to to pass a route-creating function to clients, you can pass the function returned by this call:

let functionToPassToClients = factoryFunctionIncludingUserCredentials(baseURL: "https://tumblr.com")

Why might having your factory be a curried function be more desirable than having a struct with functions for each route?

  1. Function currying makes it harder/impossible for developers in the future to make private fields public or mutable, since the inputs are frozen within the function and can not be changed or exposed.
  2. Function currying allows for partial creation of the factory without having to have an intermediary, mutable object. 

This is not to claim that this is the best ​way to use factories in Swift; this is just an example of how factories, first­-class functions, function currying, and Swift enumerations with associated values can be used to solve real world problems.