Pull to refresh

Как работать со множественными запросами. Композиция, Reducer, ФП

Reading time6 min
Views2.9K
Привет, Хабр. Меня зовут Максим, я iOS-разработчик в компании FINCH. Сегодня я покажу вам некоторые практики использования функционального программирования, которые мы наработали у себя в отделе.

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

ФП – популярная концепция, поэтому я не буду объяснять основы. Уверен, что вы и так применяете map, reduce, compactMap, first(where:) и подобные технологии в своих проектах. В статье речь пойдет о решении проблемы множественных запросов и работе с reducer.

Проблема множественных запросов


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

Иногда я мог написать что-то вроде:

networkClient.sendRequest(request1) { result in
            
    switch result {
          
    case .success(let response1):
                // ...
        self.networkClient.sendRequest(request2) { result in
                    // ...
             switch result {
                    
             case .success(let response2):
                   // ... что - то делаем со вторым response 
                   self.networkClient.sendRequest(request3) { result in
                            
                       switch result {
                       case .success(let response3):
                           // ... тут что-то делаем с конечным результатом
                           completion(Result.success(response3))
                            
                       case .failure(let error):
                           completion(Result.failure(.description(error)))
                       }

                   }

             case .failure(let error):
                 completionHandler(Result.failure(.description(error)))
             }

         }

    case .failure(let error):
        completionHandler(Result.failure(.description(error)))
    }
}

Отвратительно, правда? Но это та реальность с которой мне нужно было работать.

Мне нужно было отправить три последовательных запроса для авторизации. Во время рефакторинга я подумал, что хорошей идеей будет разбить каждый запрос на отдельные методы и вызвать их внутри completion, разгрузив, тем самым, один огромный метод. Получилось что-то вроде:


func obtainUserStatus(completion: @escaping (Result<AuthResponse>) -> Void) {
        
        let endpoint= AuthEndpoint.loginRoute
        
        networkService.request(endpoint: endpoint, cachingEnabled: false) { [weak self] (result: Result<LoginRouteResponse>) in
            
            switch result {
                
            case .success(let response):
                self?.obtainLoginResponse(response: response, completion: completion)

            case .failure(let error):
                completion(.failure(error))
                
            }
        }
        
    }

    private func obtainLoginResponse(_ response: LoginRouteResponse, completion: @escaping (Result<AuthResponse>) -> Void) {
        
       let endpoint= AuthEndpoint.login
        
        networkService.request(endpoint: endpoint, cachingEnabled: false) { [weak self] (result: Result<LoginResponse>) in
            
            switch result {
                
            case .success(let response):
                self?.obtainAuthResponse(response: response, completion: completion)

            case .failure(let error):
                completion(.failure(error))
                
            }
        }

private func obtainAuthResponse(_ response: LoginResponse, completion: @escaping (Result<AuthResponse>) -> Void) {
        
       let endpoint= AuthEndpoint.auth
        
        networkService.request(endpoint: endpoint, cachingEnabled: false) { (result: Result<AuthResponse>) in
            
    	completion(result)
        }

}

Видно, что в каждом из приватных методов мне приходится проксировать

completion: @escaping (Result<AuthResponse>) -> Void 

и это мне не очень нравится.

Тогда же мне в голову пришла мысль — «а почему бы не прибегнуть к функциональному программированию?» К тому же swift, с его магией и синтаксическим сахаром, позволяет интересно и удобоваримо разбивать код на отдельные элементы.

Композиция и Reducer


Функциональное программирование тесно связано с концепцией композиции – смешиванием, соединением чего-либо. В функциональном программировании композиция предполагает, что мы комбинируем поведение из отдельных блоков, а затем, в дальнейшем, работаем с ним.

Композиция с математической точки зрения — это что-то вроде:

func compose<A,B,C>(_ f: @escaping (A) -> B, and g: @escaping (B) -> C) -> (A) -> C {
    return { a in g(f(a)) }
}

Есть функции f и g, которые внутри себя задают выходные и входные параметры. Мы хотим получить какое-то результирующее поведение от этих входных методов.

Как пример, можно сделать два closure, один из которых увеличивает входное число на 1, а второй перемножает на само себя.

let increment: (Int) -> Int = { value in
    return value + 1
}
 
let multiply: (Int) -> Int = { value in
    return value * value
}

В результате мы хотим применить обе эти операции:


let result = compose(multiply, and: increment)
 
result(10) // в результате имеем число 101


К сожалению мой пример не является ассоциативным
( если мы поменяем местами increment и multiply, то получим число 121), но пока опустим этот момент.


let result = compose(increment, and: multiply)
 
result(10) // в результате имеем число 121

P.S. Я специально стараюсь сделать мои примеры более простыми, чтобы было максимально понятно)

На практике же часто нужно сделать что-то вроде этого:


let value: Int? = array
        
        .lazy
        .filter { $0 % 2 == 1 }
        .first(where: { $0 > 10 })

Это и есть композиция. Мы задаём входное воздействие и получаем некоторое выходное воздействие. Но это не просто сложение каких-то объектов – это сложение целого поведения.

А теперь подумаем более абстрактно :)


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

А что если создать сущность, которая как раз скомбинирует мой state и action вместе?

Так мы получим Reducer


struct Reducer<S, A> {
    let reduce: (S, A) -> S
}

На вход метода reduce мы будем давать текущий state и action, а на выходе получим новый state, который образовался внутри reduce.

Описать эту структуру мы можем несколькими способами: задав новый state, используя функциональный метод или используя мутабельные модели.


struct Reducer<S, A> {
    let reduce: (S, A) -> S
}

struct Reducer<S, A> {
    let reduce: (S) -> (A) -> S
}

struct Reducer<S, A> {
    let reduce: (inout S, A) -> Void
}

Первый вариант «классический».

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

Третий вариант – работа со state по ссылке. При таком подходе, мы не просто выдаем state, а работаем с ссылкой на объект который приходит на вход. Мне кажется, что этот способ не очень хорош, потому что подобные (мутабельные) модельки – это плохо. Лучше пересобирать новый state(instance) и возвращать его. Но для простоты и демонстрации дальнейших примеров, условимся использовать последний вариант.

Применение reducer


Применим концепцию Reducer на существующий код – создадим RequestState, затем инициализируем его, и зададим.


class RequestState {

    // MARK: - Private properties
    
    private let semaphore = DispatchSemaphore(value: 0)
    private let networkClient: NetworkClient = NetworkClientImp()
    
    // MARK: - Public methods
    
    func sendRequest<Response: Codable>(_ request: RequestProtocol, completion: ((Result<Response>) -> Void)?) {
        
        networkClient.sendRequest(request) { (result: Result<Response>) in
          
            completion?(result)   
            self.semaphore.signal()
         
        }
        
       semaphore.wait()
     
    }
    
}

Для синхронности запросов я добавил DispatchSemaphore

Идем дальше. Теперь нам нужно создать RequestAction с, допустим, тремя запросами.


enum RequestAction {
    
    case sendFirstRequest(FirstRequest)
    case sendSecondRequest(SecondRequest)
    case sendThirdRequest(ThirdRequest)
    
}

Теперь создаем Reducer, у которого есть RequestState и RequestAction. Задаем поведение – что мы хотим делать при первом, втором, третьем запросе.


let requestReducer = Reducer<RequestState, RequestAction> { state, action in
  
    switch action {
        
    case .sendFirstRequest(let request):
      
        state.sendRequest(request) { (result: Result<FirstResponse>) in
            
            // 1 Response
            
        }
      
    case .sendSecondRequest(let request):
        
        state.sendRequest(request) { (result: Result<SecondResponse>) in
            
            // 2 Response
            
        }
     
    case .sendThirdRequest(let request):
        
        state.sendRequest(request) { (result: Result<ThirdResponse>) in
            
            // 3 Response
            
        }
        
    }
    
}

В конце – вызываем эти методы. Получается более декларативный стиль, при котором видно, что идет первый, второй и третий запросы. Все читаемо и наглядно.


var state = RequestState()

requestReducer.reduce(&state, .sendFirstRequest(FirstRequest()))
requestReducer.reduce(&state, .sendSecondRequest(SecondRequest()))
requestReducer.reduce(&state, .sendThirdRequest(ThirdRequest()))

Вывод


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

Если стоит какая-то нетривиальная задача, то есть смысл посмотреть на неё с другого угла.
Tags:
Hubs:
Total votes 13: ↑10 and ↓3+7
Comments15

Articles