Como trabalhar com várias consultas. Composição, Redutor, FP

Oi Habr. Meu nome é Maxim, sou desenvolvedor iOS da FINCH. Hoje vou mostrar algumas práticas de uso de programação funcional que desenvolvemos em nosso departamento.

Quero notar imediatamente que não recomendo que você use a programação funcional em qualquer lugar - isso não é uma panacéia para todos os problemas. Mas parece-me que, em alguns casos, o FP pode fornecer as soluções mais flexíveis e elegantes para problemas fora do padrão.

FP é um conceito popular, então não vou explicar o básico. Estou certo de que você já usa map, reduza, compactMap, primeiro (onde :) e tecnologias similares em seus projetos. O artigo se concentrará em resolver o problema de várias consultas e trabalhar com redutor.

Problema com várias consultas


Trabalho na terceirização da produção e há situações em que um cliente com seus subcontratados cuida da criação de um back-end. Isso está longe de ser o back-end mais conveniente e você precisa fazer várias consultas paralelas.

Às vezes eu poderia escrever algo como:

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))) } } 

Nojento, certo? Mas essa é a realidade com a qual eu precisava trabalhar.

Eu precisava enviar três pedidos consecutivos de autorização. Durante a refatoração, pensei que seria uma boa idéia dividir cada solicitação em métodos separados e chamá-los de conclusão, descarregando assim um método enorme. Aconteceu algo como:

 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) } } 

Pode-se ver que em cada um dos métodos privados eu tenho que proxy

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

e eu realmente não gosto disso.

Então, o pensamento veio à minha mente - "por que não recorrer à programação funcional?" Além disso, o swift, com seu açúcar mágico e sintático, torna possível dividir o código em elementos individuais de uma maneira interessante e digerível.

Composição e Redutor


A programação funcional está intimamente relacionada ao conceito de composição - mistura, combinação de algo. Na programação funcional, a composição sugere que combinemos o comportamento de blocos individuais e depois, no futuro, trabalhemos com ele.

A composição do ponto de vista matemático é algo como:

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

Existem funções fe que especificam internamente os parâmetros de saída e entrada. Queremos obter algum tipo de comportamento resultante desses métodos de entrada.

Como exemplo, você pode fazer dois fechamentos, um dos quais aumenta o número de entrada em 1 e o segundo se multiplica por si só.

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

Como resultado, queremos aplicar as duas operações:

 let result = compose(multiply, and: increment) result(10) //     101 


Infelizmente meu exemplo não é associativo
(se trocarmos incremento e multiplicação, obtemos o número 121), mas por enquanto vamos omitir esse momento.

 let result = compose(increment, and: multiply) result(10) //     121 

PS: Eu tento especificamente tornar meus exemplos mais simples, para que fiquem o mais claro possível)

Na prática, você geralmente precisa fazer algo assim:

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

Essa é a composição. Definimos a ação de entrada e obtemos algum efeito de saída. Mas isso não é apenas a adição de alguns objetos - é a adição de todo um comportamento.

E agora vamos pensar mais abstratamente :)


Em nossa aplicação, temos algum tipo de estado. Pode ser a tela que o usuário vê atualmente ou os dados atuais armazenados no aplicativo, etc.
Além disso, temos ação - esta é a ação que o usuário pode executar (clique no botão, role pela coleção, feche o aplicativo etc.). Como resultado, operamos sobre esses dois conceitos e os conectamos um ao outro, ou seja, combinamos, hummm que combinamos (em algum lugar que eu já ouvi isso antes).

Mas e se você criar uma entidade que apenas combine meu estado e ação?

Então temos Redutor

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

Daremos o estado atual e a ação à entrada do método de redução e, na saída, obteremos um novo estado, que foi formado dentro de reduzir.

Podemos descrever essa estrutura de várias maneiras: definindo um novo estado, usando um método funcional ou usando modelos mutáveis.

 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 } 

A primeira opção é "clássica".

O segundo é mais funcional. O ponto é que não estamos retornando estado, mas um método que executa uma ação, que por sua vez já retorna estado. Este é essencialmente o curry do método de redução.

A terceira opção é trabalhar com o estado por referência. Com essa abordagem, não apenas emitimos estado, mas trabalhamos com uma referência ao objeto que entra. Parece-me que esse método não é muito bom, porque esses modelos (mutáveis) são ruins. É melhor reconstruir o novo estado (instância) e devolvê-lo. Mas, para simplificar e demonstrar mais exemplos, concordamos em usar a última opção.

Aplicando redutor


Aplicamos o conceito Redutor ao código existente - crie um RequestState, inicialize-o e defina-o.

 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() } } 

Para sincronização de solicitações, adicionei DispatchSemaphore

Vá em frente. Agora, precisamos criar uma RequestAction com, digamos, três solicitações.

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

Agora crie um redutor que tenha um RequestState e RequestAction. Definimos o comportamento - o que queremos fazer com o primeiro, segundo e terceiro pedidos.

 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 } } } 

No final, chamamos esses métodos. Acontece um estilo mais declarativo, no qual fica claro que os primeiro, segundo e terceiro pedidos estão chegando. Tudo é legível e claro.

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

Conclusão


Não tenha medo de aprender coisas novas e não tenha medo de aprender programação funcional. Eu acho que as melhores práticas estão na encruzilhada da tecnologia. Tente combinar e tirar melhor de diferentes paradigmas de programação.

Se houver alguma tarefa não trivial, faz sentido olhar para ela de um ângulo diferente.

Source: https://habr.com/ru/post/pt455840/


All Articles