如何处理多个查询。 组成,还原剂,FP

哈Ha 我叫Maxim,我是FINCH的iOS开发人员。 今天,我将向您展示一些使用我们部门开发的函数式编程的实践。

我想马上指出,我不敦促您在任何地方都使用函数式编程-这并不是解决所有问题的灵丹妙药。 但是在我看来,在某些情况下,FP可以为非标准问题提供最灵活,最优雅的解决方案。

FP是一个流行的概念,因此我将不介绍基本知识。 我确信您已经在项目中使用了map,reduce,compactMap,first(其中:)和类似技术。 本文将重点讨论解决多个查询的问题并使用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))) } } 

令人恶心吧? 但这是我需要工作的现实。

我需要发送三个连续的授权请求。 在重构期间,我认为将每个请求拆分为单独的方法并在完成时调用它们是一个好主意,从而卸载一个巨大的方法。 原来是这样的:

 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 

我真的不喜欢

然后这个想法浮现在我的脑海:“为什么不诉诸函数式编程?” 此外,快捷方式及其神奇的语法语法使得以有趣且易消化的方式将代码分解为各个元素成为可能。

组成和还原剂


函数式编程与组合的概念密切相关-混合,组合某些内容。 在函数式编程中,组合建议我们将各个块的行为组合在一起,然后在将来使用它。

从数学的角度来看,合成类似于:

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

有函数f和g在内部指定输出和输入参数。 我们想从这些输入法中得到某种形式的结果。

例如,您可以进行两个闭包,其中之一使输入数增加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 


不幸的是,我的榜样并不具有关联性
(如果我们交换增量并乘以,我们得到的数字为121),但现在我们暂时忽略这一点。

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

附言:我专门尝试简化示例,以使示例尽可能清晰)

实际上,您通常需要执行以下操作:

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

这就是组成。 我们设置输入动作并获得一些输出效果。 但这不仅是某些对象的添加-这是整个行为的添加。

现在让我们更抽象地思考:)


在我们的应用程序中,我们有某种状态。 这可能是用户当前看到的屏幕或存储在应用程序中的当前数据等。
此外,我们还有一个操作-这是用户可以执行的操作(单击按钮,在集合中滚动,关闭应用程序等)。 结果,我们对这两个概念进行操作并将它们彼此关联,即,我们结合hmmm,我们结合(在我已经听过的地方)。

但是,如果您创建一个将我的状态和行动结合在一起的实体,该怎么办?

所以我们得到减速器

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

我们将当前状态和操作提供给reduce方法的输入,在输出处我们将获得一个在reduce内部形成的新状态。

我们可以通过几种方式描述这种结构:通过定义新状态,使用功能方法或使用可变模型。

 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 } 

第一个选项是“经典”。

第二个更实用。 关键是我们不是在返回状态,而是采取行动的方法,该方法已经在返回状态。 这本质上是reduce方法的咖喱。

第三种选择是通过引用来处理状态。 使用这种方法,我们不仅可以发布状态,还可以对传入的对象进行引用。 在我看来,这种方法不是很好,因为这种(可变的)模型是不好的。 最好重建新状态(实例)并返回它。 但是为了简单起见和进一步示例的演示,我们同意使用后一种选项。

应用减速器


我们将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) } 

现在创建一个具有RequestState和RequestAction的Reducer。 我们设置行为-我们要如何处理第一个,第二个,第三个请求。

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

结论


不要害怕学习新事物,不要害怕学习函数式编程。 我认为最佳实践正处于技术的十字路口。 尝试合并并从不同的编程范例中更好地学习。

如果有任何不平凡的任务,那么从不同的角度看待它是有意义的。

Source: https://habr.com/ru/post/zh-CN455840/


All Articles