Using the factory pattern to avoid shared state in Swift
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! 🚀