Cara bekerja dengan beberapa kueri. Komposisi, Peredam, FP

Hai, Habr. Nama saya Maxim, saya adalah pengembang iOS di FINCH. Hari ini saya akan menunjukkan kepada Anda beberapa praktik menggunakan pemrograman fungsional yang telah kami kembangkan di departemen kami.

Saya ingin segera mencatat bahwa saya tidak mendesak Anda untuk menggunakan pemrograman fungsional di mana-mana - ini bukan obat mujarab untuk semua masalah. Tapi menurut saya, dalam beberapa kasus, FP dapat memberikan solusi yang paling fleksibel dan elegan untuk tugas-tugas non-standar.

FP adalah konsep yang populer, jadi saya tidak akan menjelaskan dasar-dasarnya. Saya yakin Anda sudah menggunakan peta, pengurangan, compactMap, pertama (di mana :) dan teknologi serupa di proyek Anda. Artikel ini akan fokus pada pemecahan masalah beberapa kueri dan bekerja dengan peredam.

Berbagai Masalah Kueri


Saya bekerja dalam outsourcing produksi, dan ada situasi ketika klien dengan subkontraktornya mengurus membuat backend. Ini jauh dari backend yang paling nyaman dan Anda harus membuat beberapa pertanyaan dan paralel.

Terkadang saya bisa menulis sesuatu seperti:

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

Menjijikkan, bukan? Tetapi inilah kenyataan yang saya butuhkan untuk bekerja.

Saya perlu mengirim tiga permintaan berturut-turut untuk otorisasi. Selama refactoring, saya pikir itu akan menjadi ide yang baik untuk membagi setiap permintaan menjadi metode yang terpisah dan memanggil mereka dalam penyelesaian, sehingga menurunkan satu metode besar. Ternyata sesuatu seperti:

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

Dapat dilihat bahwa dalam setiap metode pribadi saya harus proksi

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

dan saya tidak terlalu menyukainya.

Kemudian pikiran itu muncul di benak saya - "mengapa tidak menggunakan pemrograman fungsional?" Selain itu, cepat, dengan sihir dan gula sintaksisnya, memungkinkan untuk memecah kode menjadi elemen individu dengan cara yang menarik dan mudah dicerna.

Komposisi dan Peredam


Pemrograman fungsional terkait erat dengan konsep komposisi - pencampuran, menggabungkan sesuatu. Dalam pemrograman fungsional, komposisi menunjukkan bahwa kita menggabungkan perilaku dari blok individu, dan kemudian, di masa depan, bekerja dengannya.

Komposisi dari sudut pandang matematika adalah sesuatu seperti:

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

Ada fungsi f dan g yang secara internal menentukan parameter output dan input. Kami ingin mendapatkan semacam perilaku yang dihasilkan dari metode input ini.

Sebagai contoh, Anda dapat membuat dua penutupan, yang salah satunya meningkatkan jumlah input sebesar 1, dan yang kedua mengalikan dengan sendirinya.

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

Karenanya, kami ingin menerapkan kedua operasi ini:

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


Sayangnya contoh saya tidak asosiatif
(jika kita menukar kenaikan dan gandakan, kita mendapatkan nomor 121), tetapi untuk sekarang mari kita hilangkan momen ini.

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

PS Saya secara khusus mencoba membuat contoh saya lebih sederhana sehingga sejelas mungkin)

Dalam praktiknya, Anda sering perlu melakukan sesuatu seperti ini:

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

Ini komposisinya. Kami mengatur aksi input dan mendapatkan beberapa efek output. Tapi ini bukan hanya penambahan beberapa objek - ini adalah penambahan dari keseluruhan perilaku.

Dan sekarang mari kita berpikir lebih abstrak :)


Dalam aplikasi kita, kita memiliki semacam kondisi. Ini mungkin layar yang saat ini dilihat pengguna atau data saat ini yang disimpan dalam aplikasi, dll.
Selain itu, kami memiliki tindakan - ini adalah tindakan yang dapat dilakukan pengguna (klik tombol, gulir koleksi, tutup aplikasi, dll.). Sebagai hasilnya, kami beroperasi pada dua konsep ini dan menghubungkannya satu sama lain, yaitu, kami menggabungkan, hmmm, kami menggabungkan (di suatu tempat saya sudah mendengarnya).

Tetapi bagaimana jika Anda membuat entitas yang hanya menggabungkan keadaan dan tindakan saya bersama?

Jadi kami mendapatkan Reducer

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

Kami akan memberikan status saat ini dan tindakan untuk mengurangi input metode, dan pada output kami akan mendapatkan status baru, yang dibentuk di dalam pengurangan.

Kita dapat menggambarkan struktur ini dalam beberapa cara: dengan mendefinisikan keadaan baru, menggunakan metode fungsional atau menggunakan model yang bisa berubah.

 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 } 

Opsi pertama adalah "klasik."

Yang kedua lebih fungsional. Intinya adalah kita tidak mengembalikan status, tetapi metode yang mengambil tindakan, yang sudah pada gilirannya mengembalikan status. Ini pada dasarnya adalah kari dari metode pengurangan.

Opsi ketiga adalah bekerja dengan negara dengan referensi. Dengan pendekatan ini, kita tidak hanya mengeluarkan status, tetapi bekerja dengan referensi ke objek yang masuk. Sepertinya saya bahwa metode ini tidak terlalu baik, karena model (bisa berubah) seperti itu buruk. Lebih baik membangun kembali negara baru (instance) dan mengembalikannya. Tetapi untuk kesederhanaan dan demonstrasi contoh lebih lanjut, kami setuju untuk menggunakan opsi yang terakhir.

Menerapkan peredam


Kami menerapkan konsep Reducer ke kode yang ada - buat RequestState, lalu inisialisasi dan atur.

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

Untuk sinkronisasi permintaan saya menambahkan DispatchSemaphore

Silakan. Sekarang kita perlu membuat RequestAction dengan, katakanlah, tiga permintaan.

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

Sekarang buat Reducer yang memiliki RequestState dan RequestAction. Kami menetapkan perilaku - apa yang ingin kami lakukan dengan permintaan pertama, kedua, ketiga.

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

Pada akhirnya, kami menyebut metode ini. Ternyata gaya yang lebih deklaratif, di mana jelas bahwa permintaan pertama, kedua dan ketiga akan datang. Semuanya mudah dibaca dan jelas.

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

Kesimpulan


Jangan takut untuk belajar hal-hal baru dan jangan takut untuk belajar pemrograman fungsional. Saya pikir praktik terbaik ada di persimpangan teknologi. Cobalah untuk menggabungkan dan mengambil yang lebih baik dari berbagai paradigma pemrograman.

Jika ada tugas yang tidak sepele, maka masuk akal untuk melihatnya dari sudut yang berbeda.

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


All Articles