Connect with us

Программирование

Однонаправленный поток в Swift

Но как быть с type-safe системой управления состояниями? Как ее построить, используя возможности языка Swift?

Опубликовано

/

     
     

На этой неделе я расскажу о подходе к управлению состояниями, который я использую в своих приложениях уже много лет. Мы рассмотрим построение предсказуемой, тестируемой, отлаживаемой и модульной системы управления состояниями в Swift.

Swift способствует созданию type-safe кода, используя набор возможностей языка, позволяющих кодировать правильное поведение в системе типов. Мы стремимся моделировать наши типы таким образом, чтобы логическая ошибка становилась ошибкой времени компиляции, а не времени выполнения. Для этого используются типы значений, перечисления, опции, протоколы, дженерики, фантомные типы и т.д.

Система управления состоянием

Но как быть с type-safe системой управления состояниями? Как ее построить, используя возможности языка Swift? Мы применим те же инструменты для построения функциональной и безопасной системы управления состоянием.

import Observation

@Observable final class Store<State, Action> {
    private(set) var state: State
    private let reduce: (State, Action) -> State
    
    init(
        initialState state: State,
        reduce: @escaping (State, Action) -> State
    ) {
        self.state = state
        self.reduce = reduce
    }
    
    func send(_ action: Action) {
        state = reduce(state, action)
    }
}

Вот базовый пример системы управления состоянием, которую мы можем сделать в Swift. Мы выражаем тип Store, который определяет два общих типа: один для состояния, другой для действия. Он хранит текущее состояние в переменной, которую мы можем мутировать только внутри типа Store. Он также предоставляет нам функцию send, которая принимает в качестве параметра общее действие и мутирует текущее состояние. Весь тип помечен макросом Observable, что позволяет нам получать уведомления о каждом изменении состояния.

Предсказуемая

Первая цель при построении системы управления состоянием, которую мы хотим достичь, — сделать ее предсказуемой. Как видно из приведенного примера, единственным способом изменения состояния является отправка предопределенного действия. Все представления и контроллеры представления в вашем приложении берут экземпляр типа Store и могут читать только фактическое состояние. Не существует способа изменить что-либо в состоянии напрямую. Единственным способом обновления состояния является отправка действия.

struct ShopState: Equatable {
    var products: [String] = []
}

enum ShopAction: Equatable {
    case add(String)
    case remove(String)
}

let reduce: (ShopState, ShopAction) -> ShopState = { state, action in
    var newState = state
    
    switch action {
    case let .add(product):
        newState.products.append(product)
    case let .remove(product):
        newState.products.removeAll { $0 == product }
    }
    
    return newState
}

typealias ShopStore = Store<ShopState, ShopAction>

Функция reduce — это единственное место, содержащее логику обновления состояния. Это не означает, что нужно иметь одну функцию для всего приложения. Вы можете объединить несколько функций reduce в одну. Обычно у меня есть функция reduce для каждого функционального модуля моего приложения.

import SwiftUI

struct ShopView: View {
    @State private var store = ShopStore(
        initialState: .init(products: []),
        reduce: reduce
    )
    
    var body: some View {
        List(store.state.products, id: \.self) { product in
            Text(verbatim: product)
                .swipeActions {
                    Button(role: .destructive) {
                        store.send(.remove(product))
                    } label: {
                        Label("Delete", systemImage: "trash")
                    }
                }
        }
    }
}

Как видно из приведенного примера, мы определяем список, в котором отображаются товары. Мы также позволяем удалять товары из списка с помощью свайпов. Представление имеет доступ к состоянию только для чтения и может отображать его как есть. Единственный способ изменить состояние — использовать функцию send с одним из предопределенных в перечислении ShopAction случаев.

Такой подход называется однонаправленным потоком. Как следует из названия, движение осуществляется только в одном направлении. Представление посылает действия, магазин обновляет состояние, а представление принимает обновленное состояние. Применяя этот подход, мы делаем управление состоянием предсказуемым. Вы всегда знаете, где искать мутацию и где находится бизнес-логика вашего приложения.

Тестируемая

Как мы уже говорили, логика приложения находится в функции reduce. Это чистая функция, которая принимает в качестве параметров текущее состояние и действие, которое нужно применить, и возвращает новое состояние. И состояние, и действие являются типами значений. Обычно мы используем структуру для состояния и перечисление для действия. Это означает, что мы можем легко проверить любые функции reduce в модульных тестах.

import XCTest

final class ShopReducerTests: XCTestCase {
    func testRemove() {
        let initialState = ShopState(products: ["p1"])
        let newState = reduce(initialState, .remove("p1"))
        
        XCTAssertTrue(newState.products.isEmpty)
    }
}

Все, что нам нужно сделать, — это создать начальное состояние и вызвать функцию reduce с определенным действием. Затем мы можем убедиться, что новое состояние, возвращаемое функцией reduce, содержит все необходимые изменения.

С предварительным просмотром

Этот подход отлично работает с предварительным просмотром в Xcode. Можно создать несколько превью с различными начальными состояниями. Например, одно для пустого списка, другое — для списка товаров. При этом нет необходимости в мокировании протоколов, поскольку состояние представляет собой простую структуру, которую можно создать и поместить в хранилище для отображения в представлении.

#Preview {
    AnotherShopView(
        store: .init(
            initialState: .init(products: ["Product"]),
            reduce: reduce
        )
    )
}

#Preview {
    AnotherShopView(
        store: .init(
            initialState: .init(products: []),
            reduce: reduce
        )
    )
}

Отлаживаемая

Отладка и логирование стали очень простыми. Если что-то идет не так, вы знаете, где искать. Функция reduce — единственное место, содержащее логику приложения, и это лучшее место для размещения сообщений логов, поскольку все действия проходят через функцию reduce. Это означает, что ваши журналы никогда не пропустят ни одного изменения состояния.

Храня состояние функции в одном месте, мы можем легко отслеживать историю изменения состояния. Это поможет нам понять, какая последовательность действий привела к ошибке. Мы также можем закодировать состояние в JSON и получить его для анализа или восстановить под отладчиком для проверки ошибки.

Модульная

Многие мои коллеги сравнивают однонаправленный поток с single-state контейнером, где все состояние приложения хранится в одном экземпляре определенной структуры AppState. Да, это возможно, но это не единственный путь.

Обычно я определяю хранилище для каждой функции. Таким образом, каждая независимая функция имеет свое собственное хранилище. Это позволяет оптимизировать производительность, поскольку в случае огромного приложения наличие одного хранилища может привести к снижению производительности, когда вся иерархия приложения обновляется при каждом небольшом изменении состояния.

Ссылки

После нескольких лет создания подобных приложений эти идеи вылились в Swift-пакет под названием Swift Unidirectional Flow. Он реализует все обсуждаемые нами идеи в готовом к производству коде, поддерживающем параллелизм и другие возможности, которые могут понадобиться при создании реального приложения.

Я не настаиваю на использовании этого пакета. В частности, не следует импортировать сторонние библиотеки или фреймворки для создания основных функций вашего приложения. Но вы можете использовать его для вдохновения, чтобы построить систему управления состояниями в своем приложении с учетом ваших потребностей.

Заключение

Надеюсь, вам понравилась эта статья. Не стесняйтесь следовать за мной в Twitter и задавать свои вопросы, связанные с этой статьей. Спасибо, что прочитали, и до встречи!

Еще про однонаправленные потоки

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.

Наши партнеры:

LEGALBET

Мобильные приложения для ставок на спорт
Хорошие новости

Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: