Devlet Makinelerinin Yükselişi
Yayınlanan: 2022-03-10Şimdiden 2018 ve sayısız ön uç geliştirici hala karmaşıklık ve hareketsizliğe karşı bir savaşa öncülük ediyor. Aylarca kutsal kâseyi aradılar: hızlı ve yüksek kalitede teslim etmelerine yardımcı olacak hatasız bir uygulama mimarisi. Ben o geliştiricilerden biriyim ve yardımcı olabilecek ilginç bir şey buldum.
React ve Redux gibi araçlarla ileriye doğru güzel bir adım attık. Ancak, büyük ölçekli uygulamalarda tek başlarına yeterli değildirler. Bu makale, ön uç geliştirme bağlamında durum makineleri kavramını size tanıtacaktır. Muhtemelen birçoğunu zaten farkında olmadan inşa etmişsinizdir.
Durum Makinelerine Giriş
Durum makinesi matematiksel bir hesaplama modelidir. Bu, makinenin farklı durumlara sahip olabileceği, ancak belirli bir zamanda bunlardan yalnızca birini yerine getirdiği soyut bir kavramdır. Durum makinelerinin farklı türleri vardır. Sanırım en ünlüsü Turing makinesi. Bu sonsuz bir durum makinesidir, yani sayısız duruma sahip olabilir. Turing makinesi, çoğu durumda sınırlı sayıda durumumuz olduğundan, günümüzün UI geliştirmesine pek uymuyor. Mealy ve Moore gibi sonlu durum makinelerinin daha mantıklı olmasının nedeni budur.
Aralarındaki fark, Moore makinesinin durumunu yalnızca önceki durumuna göre değiştirmesidir. Ne yazık ki, kullanıcı etkileşimleri ve ağ süreçleri gibi birçok dış faktöre sahibiz, bu da Moore makinesinin bizim için yeterince iyi olmadığı anlamına geliyor. Aradığımız şey Mealy makinesi. Bir başlangıç durumuna sahiptir ve daha sonra girdiye ve mevcut durumuna bağlı olarak yeni durumlara geçiş yapar.
Bir durum makinesinin nasıl çalıştığını göstermenin en kolay yollarından biri bir turnikeye bakmaktır. Sınırlı sayıda durumu vardır: kilitli ve kilitsiz. İşte bize bu durumları olası girdileri ve geçişleriyle gösteren basit bir grafik.
Turnikenin başlangıç durumu kilitlenir. Ne kadar bastırırsak basalım, o kilitli durumda kalır. Ancak, ona bir jeton geçirirsek, kilidi açık duruma geçer. Bu noktada başka bir madeni para hiçbir şey yapmaz; hala kilitlenmemiş durumda olacaktır. Diğer taraftan bir itme işe yarayacak ve pas geçebilecektik. Bu eylem ayrıca makineyi ilk kilitli duruma geçirir.
Turnikeyi kontrol eden tek bir işlevi uygulamak isteseydik, muhtemelen iki argümanla karşılaşırdık: mevcut durum ve bir eylem. Redux kullanıyorsanız, bu muhtemelen size tanıdık geliyor. Mevcut durumu aldığımız ve eylemin yüküne bağlı olarak bir sonraki durumun ne olacağına karar verdiğimiz iyi bilinen redüktör işlevine benzer. Redüktör, durum makineleri bağlamında geçiştir. Aslında, bir şekilde değiştirebileceğimiz bir durumu olan herhangi bir uygulamaya durum makinesi denilebilir. Sadece her şeyi manuel olarak tekrar tekrar uyguluyoruz.
Bir Durum Makinesi Nasıl Daha İyidir?
İş yerinde Redux kullanıyoruz ve bundan oldukça memnunum. Ancak, sevmediğim kalıpları görmeye başladım. “Sevmiyorum” derken, işe yaramadıklarını kastetmiyorum. Daha çok karmaşıklık katıyorlar ve beni daha fazla kod yazmaya zorluyorlar. Deneyebileceğim bir yan projeyi üstlenmem gerekiyordu ve React ve Redux geliştirme uygulamalarımızı yeniden düşünmeye karar verdim. Beni ilgilendiren şeyler hakkında notlar almaya başladım ve bir durum makinesi soyutlamasının bu problemlerden bazılarını gerçekten çözeceğini fark ettim. Hadi başlayalım ve JavaScript'te bir durum makinesinin nasıl uygulanacağını görelim.
Basit bir probleme saldıracağız. Bir arka uç API'sinden veri almak ve bunu kullanıcıya göstermek istiyoruz. İlk adım, geçişler yerine durumlar içinde nasıl düşünüleceğini öğrenmektir. Durum makinelerine girmeden önce, böyle bir özelliği oluşturmak için iş akışım şuna benziyordu:
- Bir veri getir düğmesi görüntülüyoruz.
- Kullanıcı, verileri getir düğmesine tıklar.
- İsteği arka uca ateşleyin.
- Verileri alın ve ayrıştırın.
- Kullanıcıya göster.
- Veya bir hata varsa, işlemi tekrar tetikleyebilmemiz için hata mesajını görüntüleyin ve verileri getir düğmesini gösterin.
Doğrusal olarak düşünüyoruz ve temel olarak nihai sonuca yönelik tüm olası yönleri kapsamaya çalışıyoruz. Bir adım diğerine yol açar ve hızlı bir şekilde kodumuzu dallandırmaya başlarız. Kullanıcının düğmeye çift tıklaması veya arka ucun yanıtını beklerken kullanıcının düğmeyi tıklaması veya isteğin başarılı olmasına rağmen verilerin bozulması gibi sorunlara ne dersiniz? Bu durumlarda, muhtemelen bize ne olduğunu gösteren çeşitli bayraklarımız olurdu. Bayraklara sahip olmak, daha fazla if
cümlesi ve daha karmaşık uygulamalarda daha fazla çakışma anlamına gelir.
Bunun nedeni geçişleri düşünmemizdir. Bu geçişlerin nasıl ve hangi sırayla gerçekleştiğine odaklanıyoruz. Bunun yerine uygulamanın çeşitli durumlarına odaklanmak çok daha kolay olurdu. Kaç tane durumumuz var ve bunların olası girdileri nelerdir? Aynı örneği kullanarak:
- Boşta
Bu durumda, fetch-data butonunu gösteriyoruz, oturuyoruz ve bekliyoruz. Olası eylem:- Tıklayın
Kullanıcı butona tıkladığında, talebi arka uca tetikliyoruz ve ardından makineyi “getirme” durumuna geçiriyoruz.
- Tıklayın
- getiriliyor
İstek uçuşta ve biz oturup bekliyoruz. Eylemler şunlardır:- başarı
Veriler başarıyla gelir ve bozulmaz. Verileri bir şekilde kullanırız ve “boş” duruma geri döneriz. - arıza
Talepte bulunurken veya verileri ayrıştırırken bir hata varsa “hata” durumuna geçiyoruz.
- başarı
- hata
Bir hata mesajı gösteriyoruz ve veri getir butonunu görüntülüyoruz. Bu durum bir eylemi kabul eder:- yeniden denemek
Kullanıcı yeniden dene düğmesine tıkladığında, isteği yeniden başlatır ve makineyi "getirme" durumuna geçiririz.
- yeniden denemek
Kabaca aynı süreçleri tanımladık, ancak durumlar ve girdilerle.
Bu, mantığı basitleştirir ve daha öngörülebilir hale getirir. Ayrıca yukarıda belirtilen bazı sorunları da çözer. "Alma" durumundayken, herhangi bir tıklamayı kabul etmediğimize dikkat edin. Bu nedenle, kullanıcı düğmeyi tıklasa bile, makine bu durumdayken bu eyleme yanıt verecek şekilde yapılandırılmadığından hiçbir şey olmaz. Bu yaklaşım, kod mantığımızın öngörülemeyen dallanmasını otomatik olarak ortadan kaldırır. Bu , test ederken ele alacağımız daha az kodumuz olacağı anlamına gelir. Ayrıca, entegrasyon testi gibi bazı test türleri otomatikleştirilebilir. Uygulamamızın ne yaptığı hakkında gerçekten net bir fikre nasıl sahip olacağımızı düşünün ve tanımlanmış durumları ve geçişleri aşan ve iddialar üreten bir komut dosyası oluşturabiliriz. Bu iddialar, mümkün olan her duruma ulaştığımızı veya belirli bir yolculuğa çıktığımızı kanıtlayabilir.
Aslında, tüm olası durumları yazmak, tüm olası geçişleri yazmaktan daha kolaydır çünkü hangi durumlara ihtiyacımız olduğunu veya hangi durumlara sahip olduğumuzu biliyoruz. Bu arada, çoğu durumda, durumlar uygulamamızın iş mantığını tanımlar, oysa geçişler başlangıçta çok sık bilinmez. Yazılımımızdaki hatalar, yanlış bir durumda ve/veya yanlış zamanda gönderilen eylemlerin bir sonucudur. Uygulamamızı bilmediğimiz bir durumda bırakıyorlar ve bu da programımızı bozuyor veya yanlış davranmasına neden oluyor. Elbette böyle bir duruma düşmek istemiyoruz. Durum makineleri iyi güvenlik duvarlarıdır . Bizi bilinmeyen durumlara ulaşmaktan korurlar çünkü açıkça nasıl olduğunu söylemeden ne ve ne zaman olabileceğine dair sınırlar koyarız. Durum makinesi kavramı, tek yönlü bir veri akışıyla gerçekten iyi bir şekilde eşleşir. Birlikte, kod karmaşıklığını azaltırlar ve bir durumun nereden kaynaklandığına dair gizemi ortadan kaldırırlar.
JavaScript'te Durum Makinesi Oluşturma
Yeterince konuşma - biraz kod görelim. Aynı örneği kullanacağız. Yukarıdaki listeye dayanarak, aşağıdakilerle başlayacağız:
const machine = { 'idle': { click: function () { ... } }, 'fetching': { success: function () { ... }, failure: function () { ... } }, 'error': { 'retry': function () { ... } } }
Nesneler olarak durumlara ve işlevler olarak olası girdilerine sahibiz. Yine de ilk durum eksik. Yukarıdaki kodu şöyle değiştirelim:
const machine = { state: 'idle', transitions: { 'idle': { click: function() { ... } }, 'fetching': { success: function() { ... }, failure: function() { ... } }, 'error': { 'retry': function() { ... } } } }
Bize mantıklı gelen tüm durumları tanımladığımızda, girdiyi göndermeye ve durumu değiştirmeye hazırız. Bunu aşağıdaki iki yardımcı yöntemi kullanarak yapacağız:
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; }, ... }
dispatch
fonksiyonu, mevcut durumun geçişlerinde verilen isimle bir eylem olup olmadığını kontrol eder. Eğer öyleyse, verilen yük ile ateşler. Ayrıca, this.dispatch(<action>)
ile diğer eylemleri gönderebilmemiz veya this.changeStateTo(<new state>)
ile durumu değiştirebilmemiz için, bağlam olarak machine
birlikte action
işleyicisini de çağırıyoruz.
Örneğimizin kullanıcı yolculuğunun ardından göndermemiz gereken ilk eylem, click
. Bu eylemin işleyicisi şöyle görünür:
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');
Önce makinenin durumunu fetching
olarak değiştiriyoruz. Ardından, isteği arka uca tetikliyoruz. Bir söz veren getData
yöntemine sahip bir hizmetimiz olduğunu varsayalım. Çözüldüğünde ve veri ayrıştırma işlemi tamam olduğunda, failure
değilse de success
göndeririz.
Çok uzak çok iyi. Ardından, fetching
durumu altında success
ve failure
eylemlerini ve girdilerini uygulamamız gerekir:
transitions: { 'idle': { ... }, 'fetching': { success: function (data) { // render the data this.changeStateTo('idle'); }, failure: function (error) { this.changeStateTo('error'); } }, ... }
Beynimizi önceki süreç hakkında düşünmekten nasıl kurtardığımıza dikkat edin. Kullanıcı tıklamaları veya HTTP isteğinde neler olduğu umurumuzda değil. Uygulamanın fetching
durumunda olduğunu biliyoruz ve sadece bu iki eylemi bekliyoruz. Bu biraz izolasyonda yeni mantık yazmak gibidir.
Son bit error
durumudur. Uygulamanın hatadan kurtulabilmesi için bu yeniden deneme mantığını sağlasaydık iyi olurdu.
transitions: { 'error': { retry: function () { this.changeStateTo('idle'); this.dispatch('click'); } } }
Burada click
işleyicisinde yazdığımız mantığı çoğaltmamız gerekiyor. Bundan kaçınmak için, ya işleyiciyi her iki eylem için de erişilebilir bir işlev olarak tanımlamalıyız ya da önce idle
duruma geçmeli ve ardından click
eylemini manuel olarak göndermeliyiz.
Çalışan durum makinesinin tam bir örneği Codepen'imde bulunabilir.
Bir Kitaplık ile Durum Makinelerini Yönetme
Sonlu durumlu makine modeli, React, Vue veya Angular kullanmamızdan bağımsız olarak çalışır. Bir önceki bölümde gördüğümüz gibi, bir durum makinesini çok fazla sorun yaşamadan kolayca uygulayabiliriz. Ancak bazen bir kitaplık daha fazla esneklik sağlar. İyi olanlardan bazıları Machina.js ve XState'dir. Ancak bu yazıda, sonlu durum makineleri konseptinde çalışan Redux benzeri kitaplığım Stent hakkında konuşacağız.
Stent, bir durum makineleri konteynerinin bir uygulamasıdır. Redux ve Redux-Saga projelerindeki bazı fikirleri takip ediyor, ancak bence daha basit ve standart olmayan süreçler sağlıyor. Benioku odaklı geliştirme kullanılarak geliştirildi ve kelimenin tam anlamıyla yalnızca API tasarımında haftalar geçirdim. Kütüphaneyi yazdığım için Redux ve Flux mimarilerini kullanırken karşılaştığım sorunları düzeltme şansım oldu.
Makine Oluşturma
Çoğu durumda, uygulamalarımız birden çok alanı kapsar. Tek makine ile gidemeyiz. Böylece, Stent birçok makinenin oluşturulmasına izin verir:
import { Machine } from 'stent'; const machineA = Machine.create('A', { state: ..., transitions: ... }); const machineB = Machine.create('B', { state: ..., transitions: ... });
Daha sonra Machine.get
yöntemini kullanarak bu makinelere erişebiliriz:
const machineA = Machine.get('A'); const machineB = Machine.get('B');
Makineleri Oluşturma Mantığına Bağlama
Benim durumumda işleme React aracılığıyla yapılıyor, ancak başka herhangi bir kitaplığı kullanabiliriz. Oluşturmayı tetiklediğimiz bir geri arama başlatmak için kaynar. Üzerinde çalıştığım ilk özelliklerden biri connect
işleviydi:
import { connect } from 'stent/lib/helpers'; Machine.create('MachineA', ...); Machine.create('MachineB', ...); connect() .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { ... rendering here });
Hangi makinelerin bizim için önemli olduğunu söylüyor ve isimlerini veriyoruz. map
ilettiğimiz geri arama, başlangıçta bir kez ve daha sonra bazı makinelerin durumu her değiştiğinde tetiklenir. Render'ı tetiklediğimiz yer burasıdır. Bu noktada, bağlı makinelere doğrudan erişimimiz var, böylece mevcut durumu ve yöntemleri alabiliriz. Ayrıca, geri aramayı yalnızca bir kez başlatmak için mapOnce
ve bu ilk yürütmeyi atlamak için mapSilent
vardır.
Kolaylık sağlamak için, özellikle React entegrasyonu için bir yardımcı dışa aktarılır. connect(mapStateToProps)
gerçekten benzer.
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, eşleme geri çağrımızı çalıştırır ve bir nesne almayı bekler - props
bileşenimize destek olarak gönderilen bir nesne.
Stent Bağlamında Durum Nedir?
Şimdiye kadar devletimiz basit dizeler oldu. Ne yazık ki, gerçek dünyada, bir diziden fazlasını durumda tutmak zorundayız. Bu nedenle Stent'in durumu aslında içinde özellikleri olan bir nesnedir. Ayrılmış tek özellik name
. Diğer her şey uygulamaya özel verilerdir. Örneğin:
{ name: 'idle' } { name: 'fetching', todos: [] } { name: 'forward', speed: 120, gear: 4 }
Şimdiye kadar Stent ile olan deneyimim, durum nesnesi büyürse, muhtemelen bu ek özellikleri işleyen başka bir makineye ihtiyacımız olacağını gösteriyor. Çeşitli durumları belirlemek biraz zaman alıyor, ancak bunun daha yönetilebilir uygulamalar yazmak için ileriye doğru atılmış büyük bir adım olduğuna inanıyorum. Biraz geleceği tahmin etmek ve olası eylemlerin çerçevelerini çizmek gibi.
Devlet Makinası ile Çalışmak
Baştaki örneğe benzer şekilde, makinemizin olası (sonlu) durumlarını tanımlamamız ve olası girdileri tanımlamamız gerekiyor:
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' }; } } } });
Bir run
eylemini kabul eden ilk durumumuz idle
. Makine running
durumda olduğunda, bizi idle
duruma geri getiren stop
eylemini başlatabiliriz.
Muhtemelen daha önce uygulamamızdan dispatch
ve changeStateTo
yardımcılarını hatırlayacaksınız. Bu kitaplık aynı mantığı sağlar, ancak dahili olarak gizlidir ve bunun hakkında düşünmemize gerek yoktur. Kolaylık sağlamak için, transitions
özelliğine dayalı olarak Stent aşağıdakileri oluşturur:
- makinenin belirli bir durumda olup olmadığını kontrol etmek için yardımcı yöntemler —
idle
durumisIdle()
yöntemini üretirken,running
içinisRunning()
() yöntemine sahibiz; - eylemleri göndermek için yardımcı yöntemler:
runPlease()
vestopNow()
.
Yani, yukarıdaki örnekte bunu kullanabiliriz:
machine.isIdle(); // boolean machine.isRunning(); // boolean machine.runPlease(); // fires action machine.stopNow(); // fires action
Otomatik olarak oluşturulan metotları connect
yardımcı fonksiyonu ile birleştirerek çemberi kapatabiliyoruz. Bir kullanıcı etkileşimi, durumu güncelleyen makine girişini ve eylemini tetikler. Bu güncelleme nedeniyle, connect
geçirilen eşleme işlevi tetiklenir ve durum değişikliği hakkında bilgilendiriliriz. Daha sonra yeniden render alıyoruz.
Giriş ve Eylem İşleyicileri
Muhtemelen en önemli kısım, eylem işleyicileridir. Giriş ve değişen durumlara yanıt verdiğimiz için uygulama mantığının çoğunu yazdığımız yer burasıdır. Redux'da gerçekten sevdiğim bir şey de buraya entegre edilmiştir: redüktör işlevinin değişmezliği ve basitliği. Stent'in eylem işleyicisinin özü aynıdır. Geçerli durumu ve eylem yükünü alır ve yeni durumu döndürmesi gerekir. İşleyici hiçbir şey döndürmezse ( undefined
), makinenin durumu aynı kalır.
transitions: { 'fetching': { 'success': function (state, payload) { const todos = [ ...state.todos, payload ]; return { name: 'idle', todos }; } } }
Uzak bir sunucudan veri almamız gerektiğini varsayalım. İsteği tetikler ve makineyi fetching
durumuna geçiririz. Veriler arka uçtan geldiğinde, aşağıdaki gibi bir success
eylemi başlatırız:
machine.success({ label: '...' });
Ardından, idle
bir duruma geri dönüyoruz ve bazı verileri todos
dizisi biçiminde tutuyoruz. Eylem işleyicileri olarak ayarlanacak birkaç olası değer daha vardır. İlk ve en basit durum, sadece yeni durum haline gelen bir dizgiyi geçmemizdir.
transitions: { 'idle': { 'run': 'running' } }
Bu, run()
eylemi kullanılarak { name: 'idle' }
{ name: 'running' }
öğesine bir geçiştir. Bu yaklaşım, eşzamanlı durum geçişlerimiz olduğunda ve herhangi bir meta veriye sahip olmadığımızda kullanışlıdır. Dolayısıyla, başka bir şeyi durumda tutarsak, bu tür bir geçiş onu dışarı atar. Benzer şekilde, bir durum nesnesini doğrudan iletebiliriz:
transitions: { 'editing': { 'delete all todos': { name: 'idle', todos: [] } } }
deleteAllTodos
eylemini kullanarak editing
idle
duruma geçiyoruz.
İşlev işleyicisini zaten gördük ve eylem işleyicinin son çeşidi bir üreteç işlevidir. Redux-Saga projesinden esinlenilmiştir ve şöyle görünür:
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 }; } } } });
Jeneratörlerle deneyiminiz yoksa, bu biraz şifreli görünebilir. Ancak JavaScript'teki oluşturucular güçlü bir araçtır. Eylem işleyicimizi duraklatmamıza, durumu birden çok kez değiştirmemize ve zaman uyumsuz mantığı işlememize izin verilir.
Jeneratörlerle Eğlence
Redux-Saga ile ilk tanıştığımda, zaman uyumsuz işlemleri halletmenin aşırı karmaşık bir yolu olduğunu düşündüm. Aslında, komut tasarım modelinin oldukça akıllı bir uygulamasıdır. Bu kalıbın ana faydası, mantığın çağrılması ile fiili uygulamasını ayırmasıdır.
Başka bir deyişle, ne istediğimizi söyleriz ama nasıl olması gerektiğini söylemeyiz. Matt Hink'in blog dizisi, destanların nasıl uygulandığını anlamama yardımcı oldu ve okumanızı şiddetle tavsiye ediyorum. Aynı fikirleri Stent'e de getirdim ve bu makalenin amacı için, bir şeyler üreterek, aslında onu yapmadan ne istediğimizle ilgili talimatlar verdiğimizi söyleyeceğiz. Eylem gerçekleştirildikten sonra kontrolü geri alıyoruz.
Şu anda, birkaç şey gönderilebilir (verildi):
- makinenin durumunu değiştirmek için bir durum nesnesi (veya bir dizi);
-
call
yardımcısının çağrısı (bir söz veya başka bir üretici işlevi döndüren bir eşzamanlı işlevi kabul eder) - temel olarak, "Bunu benim için çalıştırın ve eşzamansızsa bekleyin. Bitirdikten sonra bana sonucu söyle.”; -
wait
yardımcısı çağrısı (başka bir eylemi temsil eden bir diziyi kabul eder); bu yardımcı işlevi kullanırsak, işleyiciyi duraklatır ve başka bir eylemin gönderilmesini bekleriz.
İşte varyantları gösteren bir fonksiyon:
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 }; } } }
Gördüğümüz gibi, kod senkronize görünüyor, ama aslında değil. Çözülmüş sözü beklemenin veya başka bir jeneratör üzerinde yinelemenin sıkıcı kısmını yapan sadece Stent'tir.
Stent Redux Endişelerimi Nasıl Çözüyor?
Çok Fazla Kazan Kodu
Redux (ve Flux) mimarisi, sistemimizde dolaşan eylemlere dayanır. Uygulama büyüdüğünde, genellikle birçok sabite ve eylem yaratıcısına sahip oluruz. Bu iki şey genellikle farklı klasörlerde bulunur ve kodun yürütülmesini izlemek bazen zaman alır. Ayrıca, yeni bir özellik eklerken, her zaman bir dizi eylemle uğraşmak zorunda kalırız, bu da daha fazla eylem adı ve eylem oluşturucu tanımlamak anlamına gelir.
Stent'te eylem adlarımız yoktur ve kitaplık bizim için eylem oluşturucuları otomatik olarak oluşturur:
const machine = Machine.create('todo-app', { state: { name: 'idle', todos: [] }, transitions: { 'idle': { 'add todo': function (state, todo) { ... } } } }); machine.addTodo({ title: 'Fix that bug' });
Doğrudan makinenin bir yöntemi olarak tanımlanmış machine.addTodo
eylem yaratıcısına sahibiz. Bu yaklaşım, karşılaştığım başka bir sorunu da çözdü: belirli bir eyleme yanıt veren redüktörü bulmak. Genellikle, React bileşenlerinde, addTodo
gibi eylem oluşturucu adları görürüz; ancak redüktörlerde sabit olan bir eylem türü ile çalışıyoruz. Bazen tam türü görebilmem için aksiyon yaratıcısı koduna atlamam gerekiyor. Burada hiç türümüz yok.
Öngörülemeyen Durum Değişiklikleri
Genel olarak, Redux durumu değişmez bir şekilde yönetme konusunda iyi bir iş çıkarır. Sorun Redux'un kendisinde değil, geliştiricinin herhangi bir zamanda herhangi bir eylem göndermesine izin verilmesidir. Işıkları açan bir eylemimiz var dersek, o eylemi arka arkaya iki kez ateşlemek doğru mudur? Değilse, bu sorunu Redux ile nasıl çözmemiz gerekiyor? Muhtemelen, redüktöre mantığı koruyan ve ışıkların açık olup olmadığını kontrol eden bir kod koyardık - belki de mevcut durumu kontrol eden bir if
cümlesi. Şimdi soru şu, bu redüktörün kapsamı dışında değil mi? Redüktörün bu tür uç durumlardan haberi olmalı mı?
Redux'da kaçırdığım şey, koşullu mantıkla redüktörü kirletmeden uygulamanın mevcut durumuna dayalı bir eylemin gönderilmesini durdurmanın bir yolu. Ve bu kararı, aksiyon yaratıcısının kovulduğu görünüm katmanına da götürmek istemiyorum. Stent ile bu, makine mevcut durumda bildirilmeyen eylemlere yanıt vermediği için otomatik olarak gerçekleşir. Örneğin:
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();
Makinenin belirli bir zamanda yalnızca belirli girdileri kabul etmesi, bizi garip hatalardan korur ve uygulamalarımızı daha öngörülebilir hale getirir.
Devletler, Geçişler Değil
Redux, Flux gibi, geçişler açısından düşünmemizi sağlar. Redux ile geliştirmenin zihinsel modeli, büyük ölçüde eylemlere ve bu eylemlerin redüktörlerimizdeki durumu nasıl dönüştürdüğüne bağlıdır. Bu fena değil, ancak bunun yerine durumlar açısından düşünmenin daha mantıklı olduğunu buldum - uygulamanın hangi durumlarda olabileceği ve bu durumların iş gereksinimlerini nasıl temsil ettiği.
Çözüm
Programlamada, özellikle UI geliştirmede durum makineleri kavramı benim için ufuk açıcıydı. Her yerde durum makinelerini görmeye başladım ve her zaman bu paradigmaya geçmek için bir arzum var. Daha kesin olarak tanımlanmış durumlara ve bunlar arasında geçişlere sahip olmanın faydalarını kesinlikle görüyorum. Her zaman uygulamalarımı basit ve okunabilir hale getirmenin yollarını arıyorum. Durum makinelerinin bu yönde atılmış bir adım olduğuna inanıyorum. Konsept basit ve aynı zamanda güçlü. Birçok hatayı ortadan kaldırma potansiyeline sahiptir.