Articles, podcasts and news about Swift development, by John Sundell.

Using the factory pattern to avoid shared state in Swift

Published on 03 Sep 2017

Shared state is a really common source of bugs in most apps. It's what happens when you (accidentally or by design) have multiple parts of a system that rely on the same mutable state. The challenges (and bugs) usually come from not handling changes to such a state correctly throughout the system.

This week, let's take a look at how shared state can be avoided in many situations, by using the factory pattern to create clearly separated instances that each manage their own state.

The problem

Let's say that our app contains a Request class that is used to perform requests to our backend. Its implementation looks like this:

class Request {
    enum State {
        case pending
        case ongoing
        case completed(Result)
    }

    let url: URL
    let parameters: [String : String]
    fileprivate(set) var state = State.pending

    init(url: URL, parameters: [String : String] = [:]) {
        self.url = url
        self.parameters = parameters
    }
}

We then have a DataLoader class, which a Request can be passed to in order to perform it, like this:

dataLoader.perform(request) { result in
    // Handle result
}

So, what's the problem with the above setup? Since Request not only contains information about where and how the request should be performed, but also the state of it, we can end up accidentally sharing state quite easily.

A developer not familiar with the implementation details of Request might make the assumption that it's a simple value type (it sure looks like it) that can be reused, like this:

class TodoListViewController: UIViewController {
    private let request = Request(url: .todoList)
    private let dataLoader = DataLoader()

    func loadItems() {
        dataLoader.perform(request) { [weak self] result in
            self?.render(result)
        }
    }
}

With the above setup, we can easily end up in undefined situations when loadItems is called multiple times before a pending request has been completed (we might include a search control, or a pull-to-refresh mechanism, for example, which can result in many requests). Since all requests are performed using the same instance, we will keep resetting its state, making our DataLoader very confused 😬.

One way of solving this problem is by automatically cancelling each pending request when a new one is performed. While that may solve our immediate problem here, it could definitely cause other ones, and make the API a lot more unpredictable and harder to use.

Factory methods

Instead, let's use another technique to solve the above problem, by using a factory method to avoid associating the state of a request with the original request itself. This kind of decoupling is usually what's needed when avoiding shared state, and is a good practice to create more predictable code in general.

So how would we refactor Request to use a factory method? We'll start by introducing a StatefulRequest type that is a subclass of Request, and move the state information to that, like this:

// Our Request class remains the same, minus the statefulness
class Request {
    let url: URL
    let parameters: [String : String]

    init(url: URL, parameters: [String : String] = [:]) {
        self.url = url
        self.parameters = parameters
    }
}

// We introduce a stateful type, which is private to our networking code
private class StatefulRequest: Request {
    enum State {
        case pending
        case ongoing
        case completed(Result)
    }

    var state = State.pending
}

Then, we'll add a factory method to Request that lets us construct a stateful version of a passed request:

private extension Request {
    func makeStateful() -> StatefulRequest {
        return StatefulRequest(url: url, parameters: parameters)
    }
}

Finally, when a DataLoader starts performing a request, we'll simply make it construct a new StatefulRequest each time:

class DataLoader {
    func perform(_ request: Request) {
        perform(request.makeStateful())
    }

    private func perform(_ request: StatefulRequest) {
        // Actually perform the request
        ...
    }
}

By always creating a new instance for each time a request is performed, we have now eliminated all possibilities for its state to be shared 👍.

A standard pattern

As a quick tangent, this is actually the exact same pattern that is used when iterating through sequences in Swift. Rather than sharing iteration state (such as which element is the current one), an Iterator is created to hold such state for each iteration. So when you type something like:

for book in books {
    ...
}

What happens under the hood is that Swift calls books.makeIterator(), which returns an appropriate iterator depending on the collection type. We'll look more into collections and how they work under the hood in an upcoming post.

Factories

Next up, let's take a look at another situation where the factory pattern can be used to avoid shared state, using factory types.

Let's say we're building an app about movies, where the user can list movies in categories or by getting recommendations. We'll have a view controller for each use case, which both use a singleton MovieLoader to perform requests against our backend, like this:

class CategoryViewController: UIViewController {
    // We paginate our view using section indexes, so that we
    // don't have to load all data at once
    func loadMovies(atSectionIndex sectionIndex: Int) {
        MovieLoader.shared.loadMovies(in: category, sectionIndex: sectionIndex) {
            [weak self] result in
            self?.render(result)
        }
    }
}

Using a singleton this way might not seem problematic at first (it's also very common), but we might end up in quite tricky situations if the user starts browsing around our app quicker than requests are completed. We might end up with a long queue of unfinished requests, which could make the app really slow to use - especially under poor network conditions.

What we're facing here is another problem that's the result of state (in this case the loading queue) being shared.

To solve this problem, we'll instead use a new instance of MovieLoader for each view controller. That way, we can simply have each loader cancel all pending requests when it gets deallocated, so that the queue won't be full of requests we're no longer interested in:

class MovieLoader {
    deinit {
        cancelAllRequests()
    }
}

However, we don't really want to have to manually create a new instance of MovieLoader each time we create a view controller. We probably need to inject things like a cache, a URL session, and other things that we'd have to keep passing around across view controllers. That sounds messy, let's instead use a factory!

class MovieLoaderFactory {
    private let cache: Cache
    private let session: URLSession

    // We can have the factory contain references to underlying dependencies,
    // so that we don't have to expose those details to each view controller
    init(cache: Cache, session: URLSession) {
        self.cache = cache
        self.session = session
    }

    func makeLoader() -> MovieLoader {
        return MovieLoader(cache: cache, session: session)
    }
}

Then, we'll initialize each of our view controllers with a MovieLoaderFactory, and once it needs a loader, it lazily creates one using the factory. Like this:

class CategoryViewController: UIViewController {
    private let loaderFactory: MovieLoaderFactory
    private lazy var loader: MovieLoader = self.loaderFactory.makeLoader()

    init(loaderFactory: MovieLoaderFactory) {
        self.loaderFactory = loaderFactory
        super.init(nibName: nil, bundle: nil)
    }

    private func openRecommendations(forMovie movie: Movie) {
        let viewController = RecommendationsViewController(
            movie: movie,
            loaderFactory: loaderFactory
        )

        navigationController?.pushViewController(viewController, animated: true)
    }
}

As you can see above, one big advantage of using the factory pattern here is that we can simply pass the factory along to any subsequent view controllers. We have avoided sharing state, and we're not introducing much more complexity by having to pass multiple dependencies around 🎉.

Conclusion

Factories can be a really useful tool to decouple code, both in terms of state and to create better separation of concerns. By always creating new instances, shared state can easily be avoided, and factories is a really nice way to encapsulate the creation of such instances.

We'll revisit the factory pattern in later posts, to take a look at how it can be used to solve other types of problems too.

What do you think? Have you used the factory pattern in similar situations, or do you have another technique to recommend? Let me know, along with any questions, feedback or comments you might have - on Twitter @johnsundell.

Thanks for reading! 🚀