Фабрики в Swift

Здравствуйте!
Продолжаю переводить полезные посты. В последнее время была куча статей о JavaScript и HTML/CSS, так что вот вам неплохая статья об использовании фабрик в Swift.
Оригинал статьи: «Using the factory pattern to avoid shared state in Swift».
Разделяемое состояние — действительно частый источник багов во многих приложениях. Это случается, когда вы случайно или намеренно допускаете наличие в своей архитектуре компонентов, обращающихся к одному и тому же изменяемому состоянию. Проблемы и баги обычно возникают из-за неправильной обработки изменений такого состояния в системе.
Давайте посмотрим, как можно избежать разделяемого состояния в большинстве ситуаций, используя фабрики для создания явно разделенных экземпляров, пользующихся собственными состояниями.
Проблема
Скажем, наше приложение содержит класс Request, который используется для запросов к бекенду. Его реализация выглядит примерно так:
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
}
}
Еще у нас есть класс DataLoader, в который передается Request для его выполнения, примерно так:
dataLoader.perform(request) { result in
// Handle result
}
Итак, в чем проблема? Поскольку Request содержит не только информацию о том, где и когда запрос должен быть выполнен, а еще и состояние запроса, мы легко можем получить разделение состояния. Разработчики, не знакомые с конкретной реализацией Request, могут допускать, что это значение простого типа, которое может быть переиспользовано, как тут:
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)
}
}
}
С таким кодом мы можем легко получить неопределенную ситуацию, когда loadItems вызван несколько раз, прежде чем все запросы закончатся. Например, механизм поиска или pull-to-refresh с легкостью может создавать кучу запросов. Так как все запросы выполняются, используя один и тот же экземпляр, мы продолжим обнулять состояние, оставляя DataLoader в недоумении.
Один путь решения проблемы состоит в автоматической отмене всех предыдущих запросов при создании нового. Это решит проблему, но создаст кучу новых и в итоге сделает работу с API сложнее.
Фабричные методы
Вместо этого давайте используем другую технику для решения описанной проблемы, применив фабрики, чтобы избежать связывания состояния запроса с самим изначальным запросом. Такое разделение обычно требуется, чтобы избежать разделяемого состояния, и это хорошая практика в целом.
Итак, как нам изменить Request, чтобы использовать фабрику? Мы начнем с объявления нового типа StatefulRequest, наследуемого от Request, и переместим состояние в него, примерно так:
// 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 }
Теперь добавим фабричный метод в Request, который создаст для нас StatefulRequest-версию переданного запроса:
private extension Request {
func makeStateful() -> StatefulRequest {
return StatefulRequest(url: url, parameters: parameters)
}
}
И наконец, когда DataLoader начнет выполнять запрос, мы сделаем так, чтобы он создавал новый StatefulRequest всякий раз:
class DataLoader {
func perform(_ request: Request) {
perform(request.makeStateful())
}
private func perform(_ request: StatefulRequest) {
// Actually perform the request
...
}
}
Созданием нового экземпляра для каждого нового запроса мы устранили все возможности появления разделяемого состояния 👍.
Стандартный подход
В качестве похожего примера — подход, который применяется при обходе коллекций в Swift. Вместо общего состояния итератора, например хранения указания на текущий элемент, Iterator хранит отдельное состояние для каждой отдельной итерации. Так что если вы напишете что-то вроде этого:
for book in books {
...
}
Под капотом Swift вызовет books.makeIterator(), который вернет итератор, соответствующий типу коллекции. Мы рассмотрим коллекции и итераторы в одном из следующих постов.
Фабрики
Теперь давайте посмотрим на другую ситуацию, в которой применимы фабрики для избавления от разделяемого состояния. Речь пойдет о фабричных типах.
Скажем, мы пишем приложение о фильмах, в котором пользователи могут смотреть списки фильмов по категориям или по рекомендациям. У нас есть контроллеры для каждого случая, которые используют синглтон MovieLoader для запросов к бекенду:
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)
}
}
}
Использование синглтона таким образом не выглядит опасным сперва, и это распространенная практика. Но когда мы начнем переключаться между экранами приложения быстрее, чем отрабатывают запросы, мы попадем в непростую ситуацию. У нас скопится длинная очередь незавершенных запросов, которые сильно замедлят работу приложения, особенно в условиях плохого соединения с сетью.
По сути тут мы имеем ту же проблему с разделяемым состоянием.
Для решения проблемы нам стоит использовать отдельный экземпляр MovieLoader для каждого контроллера. Таким образом мы сможем отменять запросы при уходе с соответствующего экрана, и очередь запросов не будет переполняться:
class MovieLoader {
deinit {
cancelAllRequests()
}
}
Однако мы не хотим руками создавать новый экземпляр MovieLoader каждый раз при загрузке контроллера. У нас есть вещи типа кэша, сессий и всего такого, что нужно переносить между контроллерами. Это звучит грязно, давайте лучше используем фабрику!
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)
}
}
Теперь мы инициализируем каждый контроллер с MovieLoaderFactory, и как только ему понадобится загрузчик, он создаст его, используя фабрику. Вот так:
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)
}
}
Как вы можете видеть, большое преимущество использования фабрики в том, что мы можем передать фабрику в любой последующий контроллер. Мы избежали разделяемого состояния, и не внесли лишнего усложнения в код 🎉.
Выводы
Фабрики — действительно полезная вещь для разделения кода, в контексте состояния и разделения ответственности. Разделяемое состояние легко избегается созданием новых экземпляров, и фабрики — отличный способ инкапсулировать их создание.
Мы еще вернемся к фабрикам в будущих постах, и посмотрим, какие еще проблемы можно решить с их помощью. Напоследок рекомендуем посмотреть вебинар по введению в язык Swift.
