Memanfaatkan Pemrograman Deklaratif untuk Membuat Aplikasi Web yang Dapat Dikelola
Diterbitkan: 2022-03-11Dalam artikel ini, saya menunjukkan bagaimana mengadopsi teknik pemrograman gaya deklaratif secara bijaksana dapat memungkinkan tim untuk membuat aplikasi web yang lebih mudah untuk diperluas dan dipelihara.
“…pemrograman deklaratif adalah paradigma pemrograman yang mengekspresikan logika komputasi tanpa menjelaskan aliran kontrolnya.” —Remo H. Jansen, Pemrograman Fungsional Praktis dengan TypeScript
Seperti kebanyakan masalah dalam perangkat lunak, memutuskan untuk menggunakan teknik pemrograman deklaratif dalam aplikasi Anda memerlukan evaluasi timbal balik yang cermat. Lihat salah satu artikel kami sebelumnya untuk diskusi mendalam tentang ini.
Di sini, fokusnya adalah pada bagaimana pola pemrograman deklaratif dapat diadopsi secara bertahap untuk aplikasi baru dan yang sudah ada yang ditulis dalam JavaScript, bahasa yang mendukung banyak paradigma.
Pertama, kami membahas cara menggunakan TypeScript di bagian belakang dan depan untuk membuat kode Anda lebih ekspresif dan tahan terhadap perubahan. Kami kemudian mengeksplorasi mesin finite-state (FSM) untuk merampingkan pengembangan front-end dan meningkatkan keterlibatan pemangku kepentingan dalam proses pengembangan.
FSM bukanlah teknologi baru. Mereka ditemukan hampir 50 tahun yang lalu dan populer di industri seperti pemrosesan sinyal, aeronautika, dan keuangan, di mana kebenaran perangkat lunak sangat penting. Mereka juga sangat cocok untuk masalah pemodelan yang sering muncul dalam pengembangan web modern, seperti mengoordinasikan pembaruan dan animasi keadaan asinkron yang kompleks.
Manfaat ini muncul karena adanya kendala dalam cara pengelolaan negara. Mesin status hanya dapat berada dalam satu status secara bersamaan dan memiliki status tetangga yang terbatas yang dapat ditransisikannya sebagai respons terhadap peristiwa eksternal (seperti klik mouse atau respons pengambilan). Hasilnya biasanya tingkat cacat berkurang secara signifikan. Namun, pendekatan FSM bisa jadi sulit untuk ditingkatkan agar berfungsi dengan baik dalam aplikasi besar. Ekstensi terbaru untuk FSM yang disebut statechart memungkinkan FSM kompleks untuk divisualisasikan dan diskalakan ke aplikasi yang jauh lebih besar, yang merupakan cita rasa mesin keadaan-terbatas yang menjadi fokus artikel ini. Untuk demonstrasi kami, kami akan menggunakan pustaka XState, yang merupakan salah satu solusi terbaik untuk FSM dan bagan status dalam JavaScript.
Deklaratif di Bagian Belakang dengan Node.js
Pemrograman back end server web menggunakan pendekatan deklaratif adalah topik besar dan biasanya mungkin dimulai dengan mengevaluasi bahasa pemrograman fungsional sisi server yang sesuai. Sebagai gantinya, mari kita asumsikan Anda membaca ini pada saat Anda telah memilih (atau sedang mempertimbangkan) Node.js untuk back end Anda.
Bagian ini merinci pendekatan untuk memodelkan entitas di bagian belakang yang memiliki manfaat berikut:
- Keterbacaan kode yang ditingkatkan
- Pemfaktoran ulang yang lebih aman
- Potensi peningkatan kinerja karena model tipe jaminan yang disediakan
Jaminan Perilaku Melalui Pemodelan Tipe
JavaScript
Pertimbangkan tugas mencari pengguna tertentu melalui alamat email mereka di JavaScript:
function validateEmail(email) { if (typeof email !== "string") return false; return isWellFormedEmailAddress(email); } function lookupUser(validatedEmail) { // Assume a valid email is passed in. // Safe to pass this down to the database for a user lookup.. }
Fungsi ini menerima alamat email sebagai string dan mengembalikan pengguna yang sesuai dari database ketika ada kecocokan.
Asumsinya adalah lookupUser()
hanya akan dipanggil setelah validasi dasar dilakukan. Ini adalah asumsi kunci. Bagaimana jika beberapa minggu kemudian, beberapa refactoring dilakukan dan asumsi ini tidak berlaku lagi? Semoga tes unit menangkap bug, atau kami mungkin mengirim teks tanpa filter ke database!
TypeScript (percobaan pertama)
Mari kita pertimbangkan TypeScript yang setara dengan fungsi validasi:
function validateEmail(email: string) { // No longer needed the type check (typeof email === "string"). return isWellFormedEmailAddress(email); }
Ini adalah sedikit peningkatan, dengan kompiler TypeScript telah menyelamatkan kita dari menambahkan langkah validasi runtime tambahan.
Jaminan keamanan yang dapat dibawa oleh pengetikan yang kuat belum benar-benar dimanfaatkan. Mari kita lihat itu.
TypeScript (percobaan kedua)
Mari tingkatkan keamanan tipe dan larang meneruskan string yang belum diproses sebagai input ke looukupUser
:
type ValidEmail = { value: string }; function validateEmail(input: string): Email | null { if (!isWellFormedEmailAddress(input)) return null; return { value: email }; } function lookupUser(email: ValidEmail): User { // No need to perform validation. Compiler has already ensured only valid emails have been passed in. return lookupUserInDatabase(email.value); }
Ini lebih baik, tapi rumit. Semua penggunaan ValidEmail
mengakses alamat sebenarnya melalui email.value
. TypeScript menggunakan pengetikan struktural daripada pengetikan nominal yang digunakan oleh bahasa seperti Java dan C#.
Meskipun kuat, ini berarti jenis lain yang mematuhi tanda tangan ini dianggap setara. Misalnya, jenis kata sandi berikut dapat diteruskan ke lookupUser()
tanpa keluhan dari kompiler:
type ValidPassword = { value: string }; const password = { value: "password" }; lookupUser(password); // No error.
TypeScript (percobaan ketiga)
Kita dapat mencapai pengetikan nominal dalam TypeScript menggunakan persimpangan:
type ValidEmail = string & { _: "ValidEmail" }; function validateEmail(input: string): ValidEmail { // Perform email validation checks.. return input as ValidEmail; } type ValidPassword = string & { _: "ValidPassword" }; function validatePassword(input: string): ValidPassword { ... } lookupUser("[email protected]"); // Error: expected type ValidEmail. lookupUser(validatePassword("MyPassword"); // Error: expected type ValidEmail. lookupUser(validateEmail("[email protected]")); // Ok.
Kami sekarang telah mencapai tujuan bahwa hanya string email yang divalidasi yang dapat diteruskan ke lookupUser()
.
Kiat Pro: Terapkan pola ini dengan mudah menggunakan tipe pembantu berikut:
type Opaque<K, T> = T & { __TYPE__: K }; type Email = Opaque<"Email", string>; type Password = Opaque<"Password", string>; type UserId = Opaque<"UserId", number>;
kelebihan
Dengan mengetikkan entitas dengan kuat di domain Anda, kami dapat:
- Kurangi jumlah pemeriksaan yang perlu dilakukan saat runtime, yang menghabiskan siklus CPU server yang berharga (walaupun jumlah yang sangat kecil, ini akan bertambah saat melayani ribuan permintaan per menit).
- Pertahankan lebih sedikit tes dasar karena jaminan yang diberikan oleh kompiler TypeScript.
- Manfaatkan refactoring yang dibantu editor dan compiler.
- Tingkatkan keterbacaan kode melalui peningkatan rasio signal-to-noise.
Kontra
Pemodelan tipe dilengkapi dengan beberapa pengorbanan untuk dipertimbangkan:
- Memperkenalkan TypeScript biasanya memperumit rantai alat, yang menyebabkan waktu eksekusi build dan pengujian suite yang lebih lama.
- Jika tujuan Anda adalah membuat prototipe fitur dan membawanya ke tangan pengguna secepatnya, upaya ekstra yang diperlukan untuk secara eksplisit memodelkan tipe dan menyebarkannya melalui basis kode mungkin tidak sepadan.
Kami telah menunjukkan bagaimana kode JavaScript yang ada di server atau lapisan validasi back-end/front-end bersama dapat diperluas dengan jenis untuk meningkatkan keterbacaan kode dan memungkinkan pemfaktoran ulang yang lebih aman—persyaratan penting untuk tim.
Antarmuka Pengguna Deklaratif
Antarmuka pengguna yang dikembangkan menggunakan teknik pemrograman deklaratif memfokuskan upaya untuk menggambarkan "apa" di atas "bagaimana". Dua dari tiga bahan dasar utama web, CSS dan HTML, adalah bahasa pemrograman deklaratif yang telah teruji oleh waktu dan lebih dari 1 miliar situs web.
React di-open-source oleh Facebook pada tahun 2013, dan secara signifikan mengubah arah pengembangan front-end. Ketika saya pertama kali menggunakannya, saya menyukai bagaimana saya bisa mendeklarasikan GUI sebagai fungsi dari status aplikasi. Saya sekarang dapat membuat UI yang besar dan kompleks dari blok penyusun yang lebih kecil tanpa berurusan dengan detail manipulasi DOM yang berantakan dan melacak bagian mana dari aplikasi yang perlu diperbarui sebagai tanggapan atas tindakan pengguna. Saya sebagian besar dapat mengabaikan aspek waktu ketika mendefinisikan UI dan fokus untuk memastikan aplikasi saya bertransisi dengan benar dari satu keadaan ke keadaan berikutnya.
Untuk mencapai cara yang lebih sederhana dalam mengembangkan UI, React menyisipkan lapisan abstraksi antara pengembang dan mesin/browser: DOM virtual .
Kerangka kerja UI web modern lainnya juga telah menjembatani kesenjangan ini, meskipun dengan cara yang berbeda. Misalnya, Vue menggunakan reaktivitas fungsional baik melalui getter/setter JavaScript (Vue 2) atau proxy (Vue 3). Svelte membawa reaktivitas melalui langkah kompilasi kode sumber tambahan (Svelte).
Contoh-contoh ini tampaknya menunjukkan keinginan besar dalam industri kami untuk menyediakan alat yang lebih baik dan lebih sederhana bagi pengembang untuk mengekspresikan perilaku aplikasi melalui pendekatan deklaratif.
Status dan Logika Aplikasi Deklaratif
Sementara lapisan presentasi terus berputar di sekitar beberapa bentuk HTML (misalnya, JSX di React, template berbasis HTML yang ditemukan di Vue, Angular, dan Svelte), saya mendalilkan bahwa masalah bagaimana memodelkan status aplikasi dengan cara yang mudah dimengerti oleh pengembang lain dan dapat dipelihara seiring pertumbuhan aplikasi masih belum terpecahkan. Kami melihat bukti ini melalui proliferasi perpustakaan dan pendekatan manajemen negara yang berlanjut hingga hari ini.
Situasinya diperumit dengan meningkatnya ekspektasi aplikasi web modern. Beberapa tantangan yang muncul yang harus didukung oleh pendekatan manajemen negara modern:
- Aplikasi offline-pertama menggunakan langganan lanjutan dan teknik caching
- Kode ringkas dan penggunaan kembali kode untuk persyaratan ukuran bundel yang terus menyusut
- Permintaan akan pengalaman pengguna yang semakin canggih melalui animasi fidelitas tinggi dan pembaruan waktu nyata
(Re)kemunculan Mesin dan Statechart Keadaan Hingga
Mesin kondisi terbatas telah digunakan secara luas untuk pengembangan perangkat lunak di industri tertentu di mana ketahanan aplikasi sangat penting seperti penerbangan dan keuangan. Ini juga semakin populer untuk pengembangan front-end aplikasi web melalui, misalnya, perpustakaan XState yang luar biasa.
Wikipedia mendefinisikan mesin keadaan terbatas sebagai:
Mesin abstrak yang dapat berada tepat di salah satu dari sejumlah keadaan terbatas pada waktu tertentu. FSM dapat berubah dari satu keadaan ke keadaan lain sebagai respons terhadap beberapa input eksternal; perubahan dari satu keadaan ke keadaan lain disebut transisi. FSM didefinisikan oleh daftar statusnya, status awalnya, dan kondisi untuk setiap transisi.
Dan selanjutnya:
Status adalah deskripsi status sistem yang sedang menunggu untuk melakukan transisi.
FSM dalam bentuk dasarnya tidak dapat diskalakan dengan baik ke sistem besar karena masalah ledakan keadaan. Baru-baru ini, bagan status UML dibuat untuk memperluas FSM dengan hierarki dan konkurensi, yang memungkinkan penggunaan FSM secara luas dalam aplikasi komersial.

Deklarasikan Logika Aplikasi Anda
Pertama, seperti apa FSM sebagai kode? Ada beberapa cara untuk mengimplementasikan mesin keadaan terbatas dalam JavaScript.
- Mesin keadaan terbatas sebagai pernyataan sakelar
Berikut adalah mesin yang menjelaskan kemungkinan status tempat JavaScript, diimplementasikan menggunakan pernyataan switch:
const initialState = { type: 'idle', error: undefined, result: undefined }; function transition(state = initialState, action) { switch (action) { case 'invoke': return { type: 'pending' }; case 'resolve': return { type: 'completed', result: action.value }; case 'error': return { type: 'completed', error: action.error ; default: return state; } }
Gaya kode ini akan akrab bagi pengembang yang telah menggunakan perpustakaan manajemen status Redux yang populer.
- Mesin keadaan-terbatas sebagai objek JavaScript
Inilah mesin yang sama yang diimplementasikan sebagai objek JavaScript menggunakan pustaka JavaScript XState:
const promiseMachine = Machine({ id: "promise", initial: "idle", context: { result: undefined, error: undefined, }, states: { idle: { on: { INVOKE: "pending", }, }, pending: { on: { RESOLVE: "success", REJECT: "failure", }, }, success: { type: "final", actions: assign({ result: (context, event) => event.data, }), }, failure: { type: "final", actions: assign({ error: (context, event) => event.data, }), }, }, });
Meskipun versi XState kurang ringkas, representasi objek memiliki beberapa keunggulan:
- Mesin negara itu sendiri adalah JSON sederhana, yang dapat dengan mudah dipertahankan.
- Karena bersifat deklaratif, mesin dapat divisualisasikan.
- Jika menggunakan TypeScript, kompilator memeriksa bahwa hanya transisi status yang valid yang dilakukan.
XState mendukung statechart dan mengimplementasikan spesifikasi SCXML, yang membuatnya cocok untuk digunakan dalam aplikasi yang sangat besar.
Statecharts visualisasi dari sebuah janji:
Praktik Terbaik XState
Berikut ini adalah beberapa praktik terbaik untuk diterapkan saat menggunakan XState untuk membantu menjaga proyek tetap dapat dipertahankan.
Pisahkan Efek Samping dari Logika
XState memungkinkan efek samping (yang mencakup aktivitas seperti logging atau permintaan API) untuk ditentukan secara independen dari logika mesin status.
Ini memiliki manfaat sebagai berikut:
- Bantu deteksi kesalahan logika dengan menjaga kode mesin negara tetap bersih dan sesederhana mungkin.
- Visualisasikan state machine dengan mudah tanpa perlu melepas boilerplate tambahan terlebih dahulu.
- Pengujian mesin status yang lebih mudah dengan menyuntikkan layanan tiruan.
const fetchUsersMachine = Machine({ id: "fetchUsers", initial: "idle", context: { users: undefined, error: undefined, nextPage: 0, }, states: { idle: { on: { FETCH: "fetching", }, }, fetching: { invoke: { src: (context) => fetch(`url/to/users?page=${context.nextPage}`).then((response) => response.json() ), onDone: { target: "success", actions: assign({ users: (context, event) => [...context.users, ...event.data], // Data holds the newly fetched users nextPage: (context) => context.nextPage + 1, }), }, onError: { target: "failure", error: (_, event) => event.data, // Data holds the error }, }, }, // success state.. // failure state.. }, });
Meskipun tergoda untuk menulis mesin status dengan cara ini saat Anda masih membuat semuanya berfungsi, pemisahan masalah yang lebih baik dicapai dengan memberikan efek samping sebagai opsi:
const services = { getUsers: (context) => fetch( `url/to/users?page=${context.nextPage}` ).then((response) => response.json()) } const fetchUsersMachine = Machine({ ... states: { ... fetching: { invoke: { // Invoke the side effect at key: 'getUsers' in the supplied services object. src: 'getUsers', } on: { RESOLVE: "success", REJECT: "failure", }, }, ... }, // Supply the side effects to be executed on state transitions. { services } });
Ini juga memungkinkan pengujian unit mesin status yang mudah, memungkinkan ejekan eksplisit pengambilan pengguna:
async function testFetchUsers() { return [{ name: "Peter", location: "New Zealand" }]; } const machine = fetchUsersMachine.withConfig({ services: { getUsers: (context) => testFetchUsers(), }, });
Memisahkan Mesin Besar
Tidak selalu jelas bagaimana cara terbaik untuk menyusun domain masalah ke dalam hierarki mesin keadaan-terbatas yang baik saat memulai.
Tips: Gunakan hierarki komponen UI Anda untuk membantu memandu proses ini. Lihat bagian selanjutnya tentang cara memetakan mesin status ke komponen UI.
Manfaat utama menggunakan mesin status adalah untuk secara eksplisit memodelkan semua status dan transisi antar status dalam aplikasi Anda sehingga perilaku yang dihasilkan dipahami dengan jelas, membuat kesalahan logika atau celah mudah dikenali.
Agar ini berfungsi dengan baik, mesin harus dijaga agar tetap kecil dan ringkas. Untungnya, menyusun mesin negara secara hierarkis itu mudah. Dalam contoh bagan status kanonik dari sistem lampu lalu lintas, status "merah" itu sendiri menjadi mesin status anak. Mesin "ringan" induk tidak mengetahui status internal "merah" tetapi memutuskan kapan harus memasukkan "merah" dan apa perilaku yang dimaksud saat keluar:
1-1 Pemetaan State Machines ke Stateful UI Components
Ambil, misalnya, situs eCommerce fiktif yang sangat disederhanakan yang memiliki tampilan React berikut:
<App> <SigninForm /> <RegistrationForm /> <Products /> <Cart /> <Admin> <Users /> <Products /> </Admin> </App>
Proses untuk menghasilkan mesin status yang sesuai dengan tampilan di atas mungkin tidak asing bagi mereka yang telah menggunakan pustaka manajemen status Redux:
- Apakah komponen memiliki status yang perlu dimodelkan? Misalnya, Admin/Produk tidak boleh; pengambilan halaman ke server ditambah solusi caching (seperti SWR) mungkin cukup. Di sisi lain, komponen seperti SignInForm atau Keranjang biasanya berisi status yang perlu dikelola, seperti data yang dimasukkan ke dalam bidang atau isi keranjang saat ini.
- Apakah teknik keadaan lokal (misalnya,
setState() / useState()
React's cukup untuk menangkap masalah? Melacak apakah modal popup keranjang saat ini terbuka hampir tidak memerlukan penggunaan mesin keadaan-terbatas. - Apakah mesin negara yang dihasilkan cenderung terlalu rumit? Jika demikian, pisahkan mesin menjadi beberapa yang lebih kecil, mengidentifikasi peluang untuk membuat mesin turunan yang dapat digunakan kembali di tempat lain. Misalnya, mesin SignInForm dan RegistrationForm dapat memanggil instance dari textFieldMachine anak untuk memodelkan validasi dan status untuk bidang email, nama, dan kata sandi pengguna.
Kapan Menggunakan Model Mesin Keadaan Terbatas
Sementara statechart dan FSM secara elegan dapat memecahkan beberapa masalah yang menantang, memutuskan alat dan pendekatan terbaik untuk digunakan untuk aplikasi tertentu biasanya bergantung pada beberapa faktor.
Beberapa situasi di mana menggunakan mesin kondisi terbatas bersinar:
- Aplikasi Anda menyertakan komponen entri data yang cukup besar di mana aksesibilitas atau visibilitas lapangan diatur oleh aturan yang rumit: misalnya, entri formulir di aplikasi klaim asuransi. Di sini, FSM membantu memastikan aturan bisnis diterapkan dengan kuat. Lebih lanjut, fitur visualisasi dari statechart dapat digunakan untuk membantu meningkatkan kolaborasi dengan pemangku kepentingan non-teknis dan mengidentifikasi kebutuhan bisnis yang terperinci sejak awal dalam pengembangan.
- Untuk bekerja lebih baik pada koneksi yang lebih lambat dan memberikan pengalaman fidelitas yang lebih tinggi kepada pengguna , aplikasi web harus mengelola aliran data asinkron yang semakin kompleks. FSM secara eksplisit memodelkan semua status aplikasi, dan diagram status dapat divisualisasikan untuk membantu mendiagnosis dan memecahkan masalah data asinkron.
- Aplikasi yang membutuhkan banyak animasi canggih berbasis negara. Untuk animasi yang kompleks, teknik untuk memodelkan animasi sebagai aliran peristiwa melalui waktu dengan RxJS sangat populer. Untuk banyak skenario, ini bekerja dengan baik, namun, ketika animasi kaya digabungkan dengan rangkaian kompleks status yang diketahui, FSM memberikan "titik istirahat" yang didefinisikan dengan baik di mana animasi mengalir. FSM yang dikombinasikan dengan RxJS tampaknya merupakan kombinasi yang sempurna untuk membantu menghadirkan gelombang berikutnya dari pengalaman pengguna yang ekspresif dan fidelitas tinggi.
- Aplikasi klien yang kaya seperti pengeditan foto atau video, alat pembuatan diagram, atau game di mana sebagian besar logika bisnis berada di sisi klien. FSM secara inheren dipisahkan dari kerangka kerja atau pustaka UI dan mudah untuk menulis tes untuk memungkinkan aplikasi berkualitas tinggi untuk diulang dengan cepat dan dikirimkan dengan percaya diri.
Peringatan Mesin kondisi-terbatas
- Pendekatan umum, praktik terbaik, dan API untuk pustaka statechart seperti XState adalah hal baru bagi sebagian besar pengembang front-end, yang akan membutuhkan investasi waktu dan sumber daya untuk menjadi produktif, terutama untuk tim yang kurang berpengalaman.
- Mirip dengan peringatan sebelumnya, sementara popularitas XState terus tumbuh dan didokumentasikan dengan baik, perpustakaan manajemen negara yang ada seperti Redux, MobX, atau React Context memiliki banyak pengikut yang menyediakan banyak informasi online yang belum dicocokkan oleh XState.
- Untuk aplikasi yang mengikuti model CRUD yang lebih sederhana, teknik manajemen status yang ada dikombinasikan dengan pustaka caching sumber daya yang baik seperti SWR atau React Query sudah cukup. Di sini, kendala ekstra yang diberikan FSM, meskipun sangat membantu dalam aplikasi yang kompleks, dapat memperlambat pengembangan.
- Alat ini kurang matang daripada perpustakaan manajemen negara bagian lainnya, dengan pekerjaan yang masih berlangsung pada dukungan TypeScript yang ditingkatkan dan ekstensi devtools browser.
Membungkus
Popularitas dan adopsi pemrograman deklaratif dalam komunitas pengembangan web terus meningkat.
Sementara pengembangan web modern terus menjadi lebih kompleks, perpustakaan dan kerangka kerja yang mengadopsi pendekatan pemrograman deklaratif muncul ke permukaan dengan frekuensi yang meningkat. Alasannya tampak jelas—pendekatan yang lebih sederhana dan deskriptif untuk menulis perangkat lunak perlu dibuat.
Menggunakan bahasa yang diketik dengan kuat seperti TypeScript memungkinkan entitas dalam domain aplikasi untuk dimodelkan secara ringkas dan eksplisit, yang mengurangi kemungkinan kesalahan dan jumlah kode pemeriksaan rawan kesalahan yang perlu dimanipulasi. Mengadopsi mesin finite-state dan statechart di front end memungkinkan pengembang mendeklarasikan logika bisnis aplikasi melalui transisi status, memungkinkan pengembangan alat visualisasi yang kaya dan meningkatkan peluang untuk kolaborasi erat dengan non-pengembang.
Ketika kami melakukan ini, kami mengalihkan fokus kami dari mur dan baut tentang cara kerja aplikasi ke tampilan tingkat yang lebih tinggi yang memungkinkan kami untuk lebih fokus pada kebutuhan pelanggan dan menciptakan nilai yang langgeng.