Bangkitnya Mesin Negara
Diterbitkan: 2022-03-10Ini sudah tahun 2018, dan banyak pengembang front-end yang masih memimpin pertempuran melawan kompleksitas dan imobilitas. Bulan demi bulan, mereka telah mencari cawan suci: arsitektur aplikasi bebas bug yang akan membantu mereka memberikan dengan cepat dan dengan kualitas tinggi. Saya salah satu pengembang itu, dan saya menemukan sesuatu yang menarik yang mungkin bisa membantu.
Kami telah mengambil langkah maju yang baik dengan alat-alat seperti React dan Redux. Namun, mereka tidak cukup sendiri dalam aplikasi skala besar. Artikel ini akan memperkenalkan kepada Anda konsep mesin negara dalam konteks pengembangan front-end. Anda mungkin telah membangun beberapa dari mereka tanpa menyadarinya.
Pengantar Mesin Negara
Sebuah mesin negara adalah model matematika dari perhitungan. Ini adalah konsep abstrak di mana mesin dapat memiliki status yang berbeda, tetapi pada waktu tertentu hanya memenuhi salah satunya. Ada berbagai jenis mesin negara. Yang paling terkenal, saya percaya, adalah mesin Turing. Ini adalah mesin keadaan tak terbatas, yang berarti ia dapat memiliki banyak keadaan. Mesin Turing tidak cocok dengan pengembangan UI saat ini karena dalam banyak kasus kami memiliki jumlah status yang terbatas. Inilah sebabnya mengapa mesin negara yang terbatas, seperti Mealy dan Moore, lebih masuk akal.
Perbedaan di antara mereka adalah bahwa mesin Moore mengubah statusnya hanya berdasarkan status sebelumnya. Sayangnya, kami memiliki banyak faktor eksternal, seperti interaksi pengguna dan proses jaringan, yang berarti bahwa mesin Moore juga tidak cukup baik untuk kami. Yang kami cari adalah mesin Mealy. Ini memiliki status awal dan kemudian transisi ke status baru berdasarkan input dan statusnya saat ini.
Salah satu cara termudah untuk mengilustrasikan cara kerja mesin negara adalah dengan melihat pintu putar. Ini memiliki jumlah status yang terbatas: terkunci dan tidak terkunci. Berikut adalah grafik sederhana yang menunjukkan kepada kita status ini, dengan kemungkinan input dan transisinya.

Keadaan awal pintu putar terkunci. Tidak peduli berapa kali kita mendorongnya, itu tetap dalam keadaan terkunci. Namun, jika kita memberikan koin padanya, maka ia akan beralih ke status tidak terkunci. Koin lain pada titik ini tidak akan menghasilkan apa-apa; itu akan tetap dalam keadaan tidak terkunci. Dorongan dari sisi lain akan berhasil, dan kami bisa melewatinya. Tindakan ini juga mentransisikan mesin ke status terkunci awal.
Jika kita ingin mengimplementasikan satu fungsi yang mengontrol pintu putar, kita mungkin akan berakhir dengan dua argumen: status saat ini dan tindakan. Dan jika Anda menggunakan Redux, ini mungkin terdengar asing bagi Anda. Ini mirip dengan fungsi peredam yang terkenal, di mana kami menerima status saat ini, dan berdasarkan muatan aksi, kami memutuskan apa yang akan menjadi status berikutnya. Peredam adalah transisi dalam konteks mesin negara. Bahkan, aplikasi apa pun yang memiliki status yang dapat kita ubah entah bagaimana bisa disebut mesin status. Hanya saja kami mengimplementasikan semuanya secara manual berulang-ulang.
Bagaimana Mesin Negara Lebih Baik?
Di tempat kerja, kami menggunakan Redux, dan saya cukup senang dengan itu. Namun, saya mulai melihat pola yang tidak saya sukai. Dengan "tidak suka", saya tidak bermaksud bahwa mereka tidak bekerja. Lebih dari itu mereka menambah kerumitan dan memaksa saya untuk menulis lebih banyak kode. Saya harus melakukan proyek sampingan di mana saya memiliki ruang untuk bereksperimen, dan saya memutuskan untuk memikirkan kembali praktik pengembangan React dan Redux kami. Saya mulai membuat catatan tentang hal-hal yang membuat saya khawatir, dan saya menyadari bahwa abstraksi mesin negara akan benar-benar menyelesaikan beberapa masalah ini. Mari kita masuk dan melihat bagaimana menerapkan mesin negara dalam JavaScript.
Kami akan menyerang masalah sederhana. Kami ingin mengambil data dari API back-end dan menampilkannya kepada pengguna. Langkah pertama adalah belajar bagaimana berpikir dalam keadaan, bukan transisi. Sebelum kita masuk ke mesin negara, alur kerja saya untuk membangun fitur seperti itu dulu terlihat seperti ini:
- Kami menampilkan tombol ambil-data.
- Pengguna mengklik tombol ambil data.
- Jalankan permintaan ke ujung belakang.
- Ambil data dan uraikan.
- Tunjukkan kepada pengguna.
- Atau, jika ada kesalahan, tampilkan pesan kesalahan dan tampilkan tombol ambil data sehingga kami dapat memicu proses lagi.

Kami berpikir secara linier dan pada dasarnya mencoba untuk mencakup semua kemungkinan arah ke hasil akhir. Satu langkah mengarah ke langkah lain, dan dengan cepat kita akan mulai membuat percabangan kode kita. Bagaimana dengan masalah seperti pengguna mengklik dua kali tombol, atau pengguna mengklik tombol sambil menunggu respons back end, atau permintaan berhasil tetapi datanya rusak. Dalam kasus ini, kita mungkin memiliki berbagai tanda yang menunjukkan kepada kita apa yang terjadi. Memiliki tanda berarti lebih banyak if
klausa dan, di aplikasi yang lebih kompleks, lebih banyak konflik.

Ini karena kita berpikir dalam transisi. Kami berfokus pada bagaimana transisi ini terjadi dan dalam urutan apa. Berfokus pada berbagai status aplikasi akan jauh lebih sederhana. Berapa banyak negara bagian yang kita miliki, dan apa kemungkinan inputnya? Menggunakan contoh yang sama:
- menganggur
Dalam keadaan ini, kami menampilkan tombol ambil data, duduk dan tunggu. Tindakan yang mungkin dilakukan adalah:- klik
Saat pengguna mengklik tombol, kami menjalankan permintaan ke bagian belakang dan kemudian mentransisikan mesin ke status "mengambil".
- klik
- mengambil
Permintaan sedang dalam penerbangan, dan kami duduk dan menunggu. Tindakan tersebut adalah:- kesuksesan
Data berhasil sampai dan tidak rusak. Kami menggunakan data dalam beberapa cara dan bertransisi kembali ke status "idle". - kegagalan
Jika ada kesalahan saat membuat permintaan atau menguraikan data, kami beralih ke status "kesalahan".
- kesuksesan
- kesalahan
Kami menampilkan pesan kesalahan dan menampilkan tombol ambil data. Status ini menerima satu tindakan:- mencoba kembali
Saat pengguna mengklik tombol coba lagi, kami menjalankan permintaan lagi dan mentransisikan mesin ke status "mengambil".
- mencoba kembali
Kami telah menggambarkan proses yang kira-kira sama, tetapi dengan status dan input.

Ini menyederhanakan logika dan membuatnya lebih dapat diprediksi. Ini juga memecahkan beberapa masalah yang disebutkan di atas. Perhatikan bahwa, saat kami dalam status "mengambil", kami tidak menerima klik apa pun. Jadi, bahkan jika pengguna mengklik tombol, tidak akan terjadi apa-apa karena mesin tidak dikonfigurasi untuk merespons tindakan tersebut saat dalam kondisi tersebut. Pendekatan ini secara otomatis menghilangkan percabangan tak terduga dari logika kode kita. Ini berarti kita akan memiliki lebih sedikit kode untuk dicakup saat pengujian . Juga, beberapa jenis pengujian, seperti pengujian integrasi, dapat diotomatisasi. Pikirkan bagaimana kita akan memiliki gagasan yang sangat jelas tentang apa yang dilakukan aplikasi kita, dan kita dapat membuat skrip yang melewati status dan transisi yang ditentukan dan yang menghasilkan pernyataan. Pernyataan ini dapat membuktikan bahwa kita telah mencapai setiap kemungkinan keadaan atau menempuh perjalanan tertentu.
Faktanya, menuliskan semua keadaan yang mungkin lebih mudah daripada menulis semua kemungkinan transisi karena kita tahu keadaan mana yang kita butuhkan atau miliki. Omong-omong, dalam banyak kasus, status akan menggambarkan logika bisnis aplikasi kita, sedangkan transisi sangat sering tidak diketahui di awal. Bug dalam perangkat lunak kami adalah akibat dari tindakan yang dikirim dalam keadaan yang salah dan/atau pada waktu yang salah. Mereka meninggalkan aplikasi kita dalam keadaan yang tidak kita ketahui, dan ini merusak program kita atau membuatnya berperilaku tidak benar. Tentu kita tidak ingin berada dalam situasi seperti itu. Mesin negara adalah firewall yang bagus . Mereka melindungi kita dari mencapai keadaan yang tidak diketahui karena kita menetapkan batasan untuk apa yang bisa terjadi dan kapan, tanpa secara eksplisit mengatakan bagaimana caranya. Konsep mesin negara berpasangan sangat baik dengan aliran data searah. Bersama-sama, mereka mengurangi kompleksitas kode dan menghapus misteri dari mana sebuah negara berasal.
Membuat Mesin Status Dalam JavaScript
Cukup bicara — mari kita lihat beberapa kode. Kami akan menggunakan contoh yang sama. Berdasarkan daftar di atas, kita akan mulai dengan yang berikut:
const machine = { 'idle': { click: function () { ... } }, 'fetching': { success: function () { ... }, failure: function () { ... } }, 'error': { 'retry': function () { ... } } }
Kami memiliki status sebagai objek dan kemungkinan inputnya sebagai fungsi. Namun, status awal tidak ada. Mari kita ubah kode di atas menjadi ini:
const machine = { state: 'idle', transitions: { 'idle': { click: function() { ... } }, 'fetching': { success: function() { ... }, failure: function() { ... } }, 'error': { 'retry': function() { ... } } } }
Setelah kami mendefinisikan semua status yang masuk akal bagi kami, kami siap mengirim input dan mengubah status. Kami akan melakukannya dengan menggunakan dua metode pembantu di bawah ini:
const machine = { dispatch(actionName, ...payload) { const actions = this.transitions[this.state]; const action = this.transitions[this.state][actionName]; if (action) { action.apply(machine, ...payload); } }, changeStateTo(newState) { this.state = newState; }, ... }
Fungsi dispatch
memeriksa apakah ada tindakan dengan nama yang diberikan dalam transisi status saat ini. Jika demikian, ia menembakkannya dengan muatan yang diberikan. Kami juga memanggil pengendali action
dengan machine
sebagai konteks, sehingga kami dapat mengirimkan tindakan lain dengan this.dispatch(<action>)
atau mengubah status dengan this.changeStateTo(<new state>)
.
Mengikuti perjalanan pengguna dari contoh kita, tindakan pertama yang harus kita lakukan adalah click
. Inilah yang tampak seperti pawang tindakan itu:
transitions: { 'idle': { click: function () { this.changeStateTo('fetching'); service.getData().then( data => { try { this.dispatch('success', JSON.parse(data)); } catch (error) { this.dispatch('failure', error) } }, error => this.dispatch('failure', error) ); } }, ... } machine.dispatch('click');
Pertama-tama kita ubah status mesin menjadi fetching
. Kemudian, kami memicu permintaan ke ujung belakang. Mari kita asumsikan kita memiliki layanan dengan metode getData
yang mengembalikan janji. Setelah diselesaikan dan penguraian data OK, kami mengirimkan success
, jika tidak failure
.
Sejauh ini bagus. Selanjutnya, kita harus menerapkan tindakan dan input success
dan failure
di bawah status fetching
:
transitions: { 'idle': { ... }, 'fetching': { success: function (data) { // render the data this.changeStateTo('idle'); }, failure: function (error) { this.changeStateTo('error'); } }, ... }
Perhatikan bagaimana kita membebaskan otak kita dari keharusan memikirkan proses sebelumnya. Kami tidak peduli dengan klik pengguna atau apa yang terjadi dengan permintaan HTTP. Kami tahu bahwa aplikasi dalam keadaan fetching
, dan kami mengharapkan hanya dua tindakan ini. Ini seperti menulis logika baru secara terpisah.
Bit terakhir adalah status error
. Alangkah baiknya jika kita menyediakan logika retry itu agar aplikasi bisa pulih dari kegagalan.
transitions: { 'error': { retry: function () { this.changeStateTo('idle'); this.dispatch('click'); } } }
Disini kita harus menduplikasi logika yang kita tulis di click
handler. Untuk menghindarinya, kita harus mendefinisikan handler sebagai fungsi yang dapat diakses oleh kedua tindakan, atau pertama-tama kita transisi ke status idle
dan kemudian mengirimkan tindakan click
secara manual.
Contoh lengkap mesin status kerja dapat ditemukan di Codepen saya.
Mengelola Mesin Negara Dengan Perpustakaan
Pola mesin keadaan terbatas berfungsi terlepas dari apakah kita menggunakan React, Vue, atau Angular. Seperti yang kita lihat di bagian sebelumnya, kita dapat dengan mudah mengimplementasikan state machine tanpa banyak kesulitan. Namun, terkadang perpustakaan memberikan lebih banyak fleksibilitas. Beberapa yang bagus adalah Machina.js dan XState. Namun, dalam artikel ini, kita akan berbicara tentang Stent, perpustakaan seperti Redux saya yang memanggang konsep mesin keadaan terbatas.
Stent adalah implementasi dari wadah mesin negara. Ini mengikuti beberapa ide dalam proyek Redux dan Redux-Saga, tetapi menurut saya, menyediakan proses yang lebih sederhana dan bebas boilerplate. Ini dikembangkan menggunakan pengembangan berbasis readme, dan saya benar-benar menghabiskan waktu berminggu-minggu hanya untuk desain API. Karena saya sedang menulis perpustakaan, saya memiliki kesempatan untuk memperbaiki masalah yang saya temui saat menggunakan arsitektur Redux dan Flux.

Membuat Mesin
Dalam kebanyakan kasus, aplikasi kami mencakup banyak domain. Kita tidak bisa pergi hanya dengan satu mesin. Jadi, Stent memungkinkan pembuatan banyak mesin:
import { Machine } from 'stent'; const machineA = Machine.create('A', { state: ..., transitions: ... }); const machineB = Machine.create('B', { state: ..., transitions: ... });
Nantinya, kita bisa mendapatkan akses ke mesin-mesin ini menggunakan metode Machine.get
:
const machineA = Machine.get('A'); const machineB = Machine.get('B');
Menghubungkan Mesin Ke Logika Rendering
Rendering dalam kasus saya dilakukan melalui React, tetapi kita dapat menggunakan perpustakaan lain. Ini bermuara pada menembakkan panggilan balik di mana kami memicu rendering. Salah satu fitur pertama yang saya kerjakan adalah fungsi connect
:
import { connect } from 'stent/lib/helpers'; Machine.create('MachineA', ...); Machine.create('MachineB', ...); connect() .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { ... rendering here });
Kami mengatakan mesin mana yang penting bagi kami dan memberikan nama mereka. Panggilan balik yang kami berikan ke map
diaktifkan sekali pada awalnya dan kemudian setiap kali keadaan beberapa mesin berubah. Di sinilah kita memicu rendering. Pada titik ini, kami memiliki akses langsung ke mesin yang terhubung, sehingga kami dapat mengambil status dan metode saat ini. Ada juga mapOnce
, untuk mendapatkan panggilan balik yang diaktifkan hanya sekali, dan mapSilent
, untuk melewati eksekusi awal itu.
Untuk kenyamanan, helper diekspor secara khusus untuk integrasi React. Ini sangat mirip dengan connect(mapStateToProps)
.
import React from 'react'; import { connect } from 'stent/lib/react'; class TodoList extends React.Component { render() { const { isIdle, todos } = this.props; ... } } // MachineA and MachineB are machines defined // using Machine.create function export default connect(TodoList) .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { isIdle: MachineA.isIdle, todos: MachineB.state.todos });
Stent menjalankan panggilan balik pemetaan kami dan mengharapkan untuk menerima sebuah objek — sebuah objek yang dikirim sebagai props
ke komponen React kami.
Apa Negara Dalam Konteks Stent?
Sampai saat ini, negara kita masih sederhana. Sayangnya, di dunia nyata, kita harus menyimpan lebih dari sekadar string. Inilah sebabnya mengapa status Stent sebenarnya adalah objek dengan properti di dalamnya. Satu-satunya properti yang dipesan adalah name
. Yang lainnya adalah data khusus aplikasi. Sebagai contoh:
{ name: 'idle' } { name: 'fetching', todos: [] } { name: 'forward', speed: 120, gear: 4 }
Pengalaman saya dengan Stent sejauh ini menunjukkan kepada saya bahwa jika objek status menjadi lebih besar, kita mungkin memerlukan mesin lain yang menangani properti tambahan tersebut. Mengidentifikasi berbagai status membutuhkan waktu, tetapi saya yakin ini adalah langkah maju yang besar dalam menulis aplikasi yang lebih mudah dikelola. Ini sedikit seperti memprediksi masa depan dan menggambar kerangka tindakan yang mungkin.
Bekerja Dengan Mesin Negara
Mirip dengan contoh di awal, kita harus mendefinisikan kemungkinan (terbatas) keadaan mesin kita dan menjelaskan input yang mungkin:
import { Machine } from 'stent'; const machine = Machine.create('sprinter', { state: { name: 'idle' }, // initial state transitions: { 'idle': { 'run please': function () { return { name: 'running' }; } }, 'running': { 'stop now': function () { return { name: 'idle' }; } } } });
Kami memiliki status awal kami, idle
, yang menerima tindakan run
. Setelah mesin dalam keadaan running
, kita dapat menjalankan tindakan stop
, yang membawa kita kembali ke keadaan idle
.
Anda mungkin akan mengingat helper dispatch
dan changeStateTo
dari implementasi kami sebelumnya. Pustaka ini memberikan logika yang sama, tetapi tersembunyi secara internal, dan kita tidak perlu memikirkannya. Untuk kenyamanan, berdasarkan properti transitions
, Stent menghasilkan yang berikut:
- metode pembantu untuk memeriksa apakah mesin dalam keadaan tertentu — keadaan
idle
menghasilkan metodeisIdle()
, sedangkan untukrunning
kita memilikiisRunning()
; - metode pembantu untuk mengirim tindakan:
runPlease()
danstopNow()
.
Jadi, dalam contoh di atas, kita dapat menggunakan ini:
machine.isIdle(); // boolean machine.isRunning(); // boolean machine.runPlease(); // fires action machine.stopNow(); // fires action
Menggabungkan metode yang dihasilkan secara otomatis dengan fungsi connect
utilitas, kami dapat menutup lingkaran. Interaksi pengguna memicu input dan tindakan mesin, yang memperbarui status. Karena pembaruan itu, fungsi pemetaan yang diteruskan ke connect
diaktifkan, dan kami diberi tahu tentang perubahan status. Kemudian, kita render.
Penangan Input Dan Tindakan
Mungkin bagian yang paling penting adalah penangan tindakan. Ini adalah tempat di mana kami menulis sebagian besar logika aplikasi karena kami merespons input dan status yang diubah. Sesuatu yang sangat saya sukai di Redux juga terintegrasi di sini: kekekalan dan kesederhanaan fungsi peredam. Inti dari pengendali aksi Stent adalah sama. Ia menerima status saat ini dan muatan tindakan, dan harus mengembalikan status baru. Jika pawang tidak mengembalikan apa pun ( undefined
), maka status mesin tetap sama.
transitions: { 'fetching': { 'success': function (state, payload) { const todos = [ ...state.todos, payload ]; return { name: 'idle', todos }; } } }
Mari kita asumsikan kita perlu mengambil data dari server jauh. Kami menjalankan permintaan dan mentransisikan mesin ke status fetching
. Setelah data berasal dari back end, kami menjalankan tindakan success
, seperti:
machine.success({ label: '...' });
Kemudian, kita kembali ke keadaan idle
dan menyimpan beberapa data dalam bentuk array todos
. Ada beberapa nilai lain yang mungkin untuk ditetapkan sebagai penangan tindakan. Kasus pertama dan paling sederhana adalah ketika kita hanya melewatkan string yang menjadi status baru.
transitions: { 'idle': { 'run': 'running' } }
Ini adalah transisi dari { name: 'idle' }
ke { name: 'running' }
menggunakan tindakan run()
. Pendekatan ini berguna ketika kita memiliki transisi status sinkron dan tidak memiliki data meta apa pun. Jadi, jika kita menyimpan sesuatu yang lain dalam status, jenis transisi itu akan menghapusnya. Demikian pula, kita dapat melewatkan objek status secara langsung:
transitions: { 'editing': { 'delete all todos': { name: 'idle', todos: [] } } }
Kami sedang bertransisi dari editing
ke idle
menggunakan tindakan deleteAllTodos
.
Kita sudah melihat handler fungsi, dan varian terakhir dari handler aksi adalah fungsi generator. Ini terinspirasi oleh proyek Redux-Saga, dan terlihat seperti ini:
import { call } from 'stent/lib/helpers'; Machine.create('app', { 'idle': { 'fetch data': function * (state, payload) { yield { name: 'fetching' } try { const data = yield call(requestToBackend, '/api/todos/', 'POST'); return { name: 'idle', data }; } catch (error) { return { name: 'error', error }; } } } });
Jika Anda tidak memiliki pengalaman dengan generator, ini mungkin terlihat agak samar. Tetapi generator dalam JavaScript adalah alat yang ampuh. Kami diizinkan untuk menjeda penangan tindakan kami, mengubah status beberapa kali dan menangani logika asinkron.
Bersenang-senang Dengan Generator
Ketika saya pertama kali diperkenalkan ke Redux-Saga, saya pikir itu adalah cara yang terlalu rumit untuk menangani operasi async. Faktanya, ini adalah implementasi yang cukup cerdas dari pola desain perintah. Manfaat utama dari pola ini adalah memisahkan pemanggilan logika dan implementasi aktualnya.
Dengan kata lain, kita mengatakan apa yang kita inginkan tetapi bukan bagaimana hal itu harus terjadi. Seri blog Matt Hink membantu saya memahami bagaimana saga diimplementasikan, dan saya sangat menyarankan untuk membacanya. Saya membawa ide yang sama ke Stent, dan untuk tujuan artikel ini, kami akan mengatakan bahwa dengan menghasilkan barang, kami memberikan instruksi tentang apa yang kami inginkan tanpa benar-benar melakukannya. Setelah tindakan dilakukan, kami menerima kontrol kembali.
Saat ini, beberapa hal dapat dikirim (menghasilkan):
- objek keadaan (atau string) untuk mengubah keadaan mesin;
-
call
penolong panggilan (ia menerima fungsi sinkron, yang merupakan fungsi yang mengembalikan janji atau fungsi generator lain) — kita pada dasarnya mengatakan, “Jalankan ini untuk saya, dan jika tidak sinkron, tunggu. Setelah Anda selesai, beri saya hasilnya.”; - panggilan dari
wait
helper (ia menerima string yang mewakili tindakan lain); jika kami menggunakan fungsi utilitas ini, kami menjeda handler dan menunggu tindakan lain dikirim.
Berikut adalah fungsi yang menggambarkan varian:
const fireHTTPRequest = function () { return new Promise((resolve, reject) => { // ... }); } ... transitions: { 'idle': { 'fetch data': function * () { yield 'fetching'; // sets the state to { name: 'fetching' } yield { name: 'fetching' }; // same as above // wait for getTheData and checkForErrors actions // to be dispatched const [ data, isError ] = yield wait('get the data', 'check for errors'); // wait for the promise returned by fireHTTPRequest // to be resolved const result = yield call(fireHTTPRequest, '/api/data/users'); return { name: 'finish', users: result }; } } }
Seperti yang kita lihat, kode terlihat sinkron, tetapi sebenarnya tidak. Itu hanya Stent yang melakukan bagian yang membosankan dengan menunggu janji yang diselesaikan atau mengulangi generator lain.
Bagaimana Stent Memecahkan Masalah Redux Saya
Terlalu Banyak Kode Boilerplate
Arsitektur Redux (dan Flux) bergantung pada tindakan yang beredar di sistem kami. Saat aplikasi berkembang, biasanya kita memiliki banyak konstanta dan pembuat tindakan. Kedua hal ini sangat sering berada di folder yang berbeda, dan melacak eksekusi kode terkadang membutuhkan waktu. Juga, saat menambahkan fitur baru, kami selalu harus berurusan dengan seluruh rangkaian tindakan, yang berarti menentukan lebih banyak nama tindakan dan pembuat tindakan.
Di Stent, kami tidak memiliki nama tindakan, dan perpustakaan membuat pembuat tindakan secara otomatis untuk kami:
const machine = Machine.create('todo-app', { state: { name: 'idle', todos: [] }, transitions: { 'idle': { 'add todo': function (state, todo) { ... } } } }); machine.addTodo({ title: 'Fix that bug' });
Kami memiliki pembuat tindakan machine.addTodo
yang didefinisikan secara langsung sebagai metode mesin. Pendekatan ini juga memecahkan masalah lain yang saya hadapi: menemukan peredam yang merespons tindakan tertentu. Biasanya, dalam komponen React, kita melihat nama pembuat tindakan seperti addTodo
; namun, dalam reduksi, kami bekerja dengan jenis tindakan yang konstan. Terkadang saya harus melompat ke kode pembuat tindakan hanya agar saya dapat melihat jenis yang tepat. Di sini, kami tidak memiliki tipe sama sekali.
Perubahan Status yang Tidak Dapat Diprediksi
Secara umum, Redux melakukan pekerjaan yang baik dalam mengelola keadaan dengan cara yang tidak dapat diubah. Masalahnya bukan di Redux itu sendiri, tetapi pengembang diizinkan untuk mengirim tindakan apa pun kapan saja. Jika kita mengatakan bahwa kita memiliki tindakan yang menyalakan lampu, apakah boleh untuk menembakkan tindakan itu dua kali berturut-turut? Jika tidak, lalu bagaimana kita harus menyelesaikan masalah ini dengan Redux? Yah, kami mungkin akan memasukkan beberapa kode ke dalam peredam yang melindungi logika dan yang memeriksa apakah lampu sudah menyala — mungkin klausa if
yang memeriksa status saat ini. Sekarang pertanyaannya adalah, bukankah ini di luar jangkauan peredam? Haruskah peredam tahu tentang kasing tepi seperti itu?
Apa yang saya lewatkan di Redux adalah cara untuk menghentikan pengiriman tindakan berdasarkan status aplikasi saat ini tanpa mencemari peredam dengan logika kondisional. Dan saya juga tidak ingin mengambil keputusan ini ke lapisan tampilan, tempat pembuat tindakan dipecat. Dengan Stent, ini terjadi secara otomatis karena mesin tidak merespons tindakan yang tidak dideklarasikan dalam status saat ini. Sebagai contoh:
const machine = Machine.create('app', { state: { name: 'idle' }, transitions: { 'idle': { 'run': 'running', 'jump': 'jumping' }, 'running': { 'stop': 'idle' } } }); // this is fine machine.run(); // This will do nothing because at this point // the machine is in a 'running' state and there is // only 'stop' action there. machine.jump();
Fakta bahwa mesin hanya menerima input tertentu pada waktu tertentu melindungi kita dari bug aneh dan membuat aplikasi kita lebih dapat diprediksi.
Serikat, Bukan Transisi
Redux, seperti Flux, membuat kita berpikir tentang transisi. Model mental pengembangan dengan Redux cukup banyak didorong oleh tindakan dan bagaimana tindakan ini mengubah keadaan di reduksi kami. Itu tidak buruk, tetapi saya merasa lebih masuk akal untuk berpikir dalam hal status — status apa yang mungkin dimiliki aplikasi dan bagaimana status ini mewakili persyaratan bisnis.
Kesimpulan
Konsep mesin negara dalam pemrograman, terutama dalam pengembangan UI, membuka mata saya. Saya mulai melihat mesin negara di mana-mana, dan saya memiliki keinginan untuk selalu beralih ke paradigma itu. Saya benar-benar melihat manfaat memiliki status dan transisi yang lebih ketat di antara mereka. Saya selalu mencari cara untuk membuat aplikasi saya sederhana dan mudah dibaca. Saya percaya bahwa mesin negara adalah langkah ke arah ini. Konsepnya sederhana dan sekaligus kuat. Ini memiliki potensi untuk menghilangkan banyak bug.