Tujuan artikel ini adalah untuk mengilustrasikan mengapa pemrograman reaktif diperlukan, bagaimana hubungannya dengan pemrograman fungsional, dan bagaimana menggunakannya untuk menulis kode deklaratif yang mudah untuk beradaptasi dengan persyaratan baru. Selain itu, saya ingin melakukan ini sesingkat dan sesederhana mungkin dengan contoh yang mendekati nyata.
Ambil tugas berikut:
Ada layanan tertentu dengan REST API dan endpoint /people
. Permintaan POST ke titik akhir ini membuat entitas baru. Tulis fungsi yang mengambil larik objek bentuk { name: 'Max' }
dan buat satu set entitas menggunakan API (dalam bahasa Inggris, ini disebut operasi batch).
Mari kita selesaikan masalah ini dengan gaya imperatif:
const request = require('superagent') function batchCreate(bodies) { const calls = [] for (let body of bodies) { calls.push( request .post('/people') .send(body) .then(r => r.status) ) } return Promise.all(calls) }
Sebagai perbandingan, mari kita menulis ulang bagian kode ini dengan gaya fungsional. Untuk kesederhanaan, yang kami maksud dengan gaya fungsional:
- Penggunaan primitif fungsional ( .map , .filter , .reduce ) alih-alih loop keharusan ( untuk , sementara )
- Kode diatur ke dalam fungsi "murni" - mereka hanya bergantung pada argumen mereka dan tidak bergantung pada kondisi sistem
Kode gaya fungsional:
const request = require('superagent') function batchCreate(bodies) { const calls = bodies.map(body => request .post('/people') .send(body) .then(r => r.status) ) return Promise.all(calls) }
Kami mendapat sepotong kode dengan ukuran yang sama dan perlu diakui bahwa tidak jelas bagaimana bagian ini lebih baik daripada yang sebelumnya.
Untuk memahami mengapa bagian kedua dari kode lebih baik - Anda harus mulai mengubah kode, bayangkan bahwa persyaratan baru telah muncul untuk tugas asli:
Layanan yang kami panggil memiliki batas jumlah permintaan dalam periode waktu: dalam satu detik, satu klien dapat mengeksekusi tidak lebih dari lima permintaan. Mengeksekusi lebih banyak permintaan akan menyebabkan layanan mengembalikan kesalahan HTTP 429 (terlalu banyak permintaan).
Pada titik ini, Anda mungkin perlu berhenti dan mencoba menyelesaikan sendiri masalahnya,% nama pengguna%
Mari kita ambil kode fungsional kita sebagai basis dan cobalah mengubahnya. Masalah utama pemrograman fungsional "murni" adalah ia tidak "tahu" apa pun - tentang runtime dan input-output (dalam bahasa Inggris ada ekspresi untuk efek samping untuk ini), tetapi dalam praktiknya kami terus-menerus bekerja dengan mereka.
Untuk mengisi celah ini, Pemrograman Reaktif datang ke penyelamatan - serangkaian pendekatan yang mencoba untuk memecahkan masalah efek samping. Implementasi yang paling terkenal dari paradigma ini adalah perpustakaan Rx menggunakan konsep aliran reaktif .
Apa itu aliran reaktif? Jika sangat singkat, maka ini adalah pendekatan yang memungkinkan Anda untuk menerapkan primitif fungsional (.map, .filter, .reduce) ke sesuatu yang didistribusikan dari waktu ke waktu.
Sebagai contoh, kami mengirim serangkaian perintah tertentu melalui jaringan - kami tidak perlu menunggu sampai kami mendapatkan seluruh rangkaian, kami menyajikannya sebagai aliran reaktif dan kami dapat bekerja dengannya. Dua konsep penting lainnya muncul di sini:
- alirannya bisa tak terbatas atau didistribusikan secara sewenang-wenang dari waktu ke waktu
- pihak pengirim mengirimkan perintah hanya jika pihak penerima siap untuk memprosesnya (tekanan balik)
Tujuan artikel ini adalah untuk menemukan cara mudah, oleh karena itu, kami akan mengambil perpustakaan Highland , yang mencoba memecahkan masalah yang sama dengan Rx, tetapi jauh lebih mudah untuk dipelajari. Gagasan di dalamnya sederhana: mari kita ambil aliran Node.js sebagai basis dan "transfer" data dari satu Stream ke Stream lainnya.
Mari kita mulai: mari kita mulai dengan yang sederhana - kita akan membuat kode kita "reaktif" tanpa menambahkan fungsionalitas baru
const request = require('superagent') const H = require('highland') function batchCreate(bodies) { return H(bodies) .flatMap(body => H(request .post('localhost:3000/people') .send(body) .then(r => r.status) ) ) .collect() .toPromise(Promise) }
Apa yang harus Anda perhatikan:
- H (tubuh) - kami membuat aliran dari array
- .flatMap dan panggilan balik yang diterimanya. Idenya cukup sederhana - kita membungkus Janji dalam konstruktor aliran untuk mendapatkan aliran dengan satu nilai (atau kesalahan. Penting untuk memahami bahwa ini adalah nilai, bukan Janji).
Sebagai hasilnya, ini memberi kita aliran arus - menggunakan flatMap kita melicinkannya menjadi satu aliran nilai yang dapat kita operasikan (siapa bilang monad?) - .collect - kita perlu mengumpulkan semua nilai dalam satu "titik" ke dalam array
- .toPromise - akan kembali kepada kami Janji, yang akan dipenuhi saat kami memiliki nilai dari aliran
Sekarang mari kita coba menerapkan persyaratan kami:
const request = require('superagent') const H = require('highland') function batchCreate(bodies) { return H(bodies) .flatMap(body => H(request .post('localhost:3000/people') .send(body) .then(r => r.status) ) ) .ratelimit(5, 1000) .collect() .toPromise(Promise) }
Berkat konsep backpressure, ini hanya satu baris. Pengantar dalam paradigma ini. Dalam Rx, dibutuhkan jumlah ruang yang sama .
Nah itu saja, pendapat Anda menarik:
- Apakah saya berhasil mencapai hasil yang dinyatakan pada awal artikel?
- Apakah mungkin untuk mencapai hasil yang serupa menggunakan pendekatan imperatif?
- Apakah Anda tertarik pada pemrograman Reaktif?
PS: di sini Anda dapat menemukan artikel saya yang lain tentang Pemrograman Reaktif