صعود آلات الدولة
نشرت: 2022-03-10إنه 2018 بالفعل ، ولا يزال عدد لا يحصى من مطوري الواجهة الأمامية يقودون معركة ضد التعقيد وعدم الحركة. شهرًا بعد شهر ، بحثوا عن الكأس المقدسة: تصميم تطبيق خالٍ من الأخطاء سيساعدهم على تقديم المنتجات بسرعة وبجودة عالية. أنا أحد هؤلاء المطورين ، وقد وجدت شيئًا مثيرًا للاهتمام قد يساعد.
لقد اتخذنا خطوة جيدة للأمام باستخدام أدوات مثل React و Redux. ومع ذلك ، فهي ليست كافية بمفردها في التطبيقات واسعة النطاق. ستقدم لك هذه المقالة مفهوم آلات الحالة في سياق تطوير الواجهة الأمامية. ربما تكون قد قمت ببناء العديد منها بالفعل دون أن تدرك ذلك.
مقدمة لآلات الدولة
آلة الحالة هي نموذج رياضي للحساب. إنه مفهوم مجرد حيث يمكن للآلة أن يكون لها حالات مختلفة ، ولكن في وقت معين تحقق حالة واحدة فقط. هناك أنواع مختلفة من آلات الدولة. الأكثر شهرة ، على ما أعتقد ، هي آلة تورينج. إنها آلة دولة لا نهائية ، مما يعني أنه يمكن أن يكون لها عدد لا يحصى من الحالات. لا تتناسب آلة Turing بشكل جيد مع تطوير واجهة المستخدم اليوم لأنه في معظم الحالات لدينا عدد محدود من الحالات. هذا هو السبب في أن آلات الحالة المحدودة ، مثل Mealy و Moore ، أكثر منطقية.
الفرق بينهما هو أن آلة مور تغير حالتها بناءً على حالتها السابقة فقط. لسوء الحظ ، لدينا الكثير من العوامل الخارجية ، مثل تفاعلات المستخدم وعمليات الشبكة ، مما يعني أن آلة Moore ليست جيدة بما يكفي بالنسبة لنا أيضًا. ما نبحث عنه هو آلة الدقيقي. لها حالة أولية ثم تنتقل إلى حالات جديدة بناءً على المدخلات وحالتها الحالية.
من أسهل الطرق لتوضيح كيفية عمل آلة الدولة النظر إلى الباب الدوار. لديها عدد محدود من الحالات: مغلق وغير مقفل. هنا رسم بسيط يوضح لنا هذه الحالات مع مدخلاتها وانتقالاتها المحتملة.
تم قفل الحالة الأولية للبوابة الدوارة. بغض النظر عن عدد المرات التي قد ندفعها ، فإنها تظل في حالة القفل تلك. ومع ذلك ، إذا مررنا عملة لها ، فإنها تنتقل إلى حالة إلغاء القفل. عملة أخرى في هذه المرحلة لن تفعل شيئًا ؛ ستظل في حالة غير مؤمنة. دفعة من الجانب الآخر ستنجح ، وسنكون قادرين على التمرير. يؤدي هذا الإجراء أيضًا إلى نقل الجهاز إلى حالة القفل الأولية.
إذا أردنا تنفيذ وظيفة واحدة تتحكم في الباب الدوار ، فربما ينتهي بنا الأمر مع وسيطتين: الحالة الحالية والإجراء. وإذا كنت تستخدم Redux ، فمن المحتمل أن يبدو هذا مألوفًا لك. إنها تشبه وظيفة المخفض المعروفة ، حيث نتلقى الحالة الحالية ، وبناءً على حمولة الإجراء ، نقرر ما ستكون الحالة التالية. المخفض هو الانتقال في سياق آلات الحالة. في الواقع ، أي تطبيق له حالة يمكننا تغييرها بطريقة ما يمكن أن يسمى آلة الحالة. كل ما في الأمر أننا ننفذ كل شيء يدويًا مرارًا وتكرارًا.
كيف تكون آلة الدولة أفضل؟
في العمل ، نستخدم Redux ، وأنا سعيد جدًا به. ومع ذلك ، بدأت أرى أنماطًا لا أحبها. بعبارة "لا أحب" ، لا أعني أنهم لا يعملون. إنها أكثر من أنها تضيف تعقيدًا وتجبرني على كتابة المزيد من التعليمات البرمجية. اضطررت إلى تنفيذ مشروع جانبي كان لدي مجال للتجربة فيه ، وقررت إعادة التفكير في ممارسات تطوير React و Redux. بدأت في تدوين الملاحظات حول الأشياء التي تقلقني ، وأدركت أن تجريد آلة الدولة من شأنه حقًا حل بعض هذه المشكلات. دعنا نقفز ونرى كيفية تنفيذ آلة الدولة في JavaScript.
سنهاجم مشكلة بسيطة. نريد جلب البيانات من واجهة API الخلفية وعرضها على المستخدم. الخطوة الأولى هي تعلم كيفية التفكير في الدول ، بدلاً من التحولات. قبل الدخول في أجهزة الحالة ، كان سير العمل الخاص بي لبناء مثل هذه الميزة يبدو كالتالي:
- نعرض زر جلب البيانات.
- ينقر المستخدم على زر جلب البيانات.
- أطلق الطلب إلى النهاية الخلفية.
- استرجع البيانات وتحليلها.
- أظهرها للمستخدم.
- أو ، إذا كان هناك خطأ ، فقم بعرض رسالة الخطأ وإظهار زر جلب البيانات حتى نتمكن من تشغيل العملية مرة أخرى.
نحن نفكر بشكل خطي ونحاول بشكل أساسي تغطية جميع الاتجاهات الممكنة للنتيجة النهائية. خطوة واحدة تؤدي إلى أخرى ، وسرعان ما نبدأ في تفريع الكود الخاص بنا. ماذا عن المشكلات مثل قيام المستخدم بالنقر المزدوج فوق الزر ، أو قيام المستخدم بالنقر فوق الزر أثناء انتظار استجابة الطرف الخلفي ، أو نجاح الطلب ولكن البيانات تالفة. في هذه الحالات ، سيكون لدينا على الأرجح أعلام مختلفة توضح لنا ما حدث. وجود علامات يعني المزيد if
البنود ، وفي التطبيقات الأكثر تعقيدًا ، المزيد من التعارضات.
هذا لأننا نفكر في التحولات. نحن نركز على كيفية حدوث هذه التحولات وبأي ترتيب. سيكون التركيز بدلاً من ذلك على حالات التطبيق المختلفة أسهل كثيرًا. كم عدد الولايات لدينا ، وما هي مدخلاتها المحتملة؟ باستخدام نفس المثال:
- عاطل
في هذه الحالة ، نعرض زر جلب البيانات ، واجلس وانتظر. الإجراء المحتمل هو:- انقر
عندما ينقر المستخدم على الزر ، فإننا نطلق الطلب إلى النهاية الخلفية ثم نحول الجهاز إلى حالة "الجلب".
- انقر
- الجلب
الطلب في رحلة ونحن نجلس وننتظر. الإجراءات هي:- نجاح
تصل البيانات بنجاح وهي غير تالفة. نستخدم البيانات بطريقة ما وننتقل مرة أخرى إلى حالة "الخمول". - بالفشل
إذا كان هناك خطأ أثناء تقديم الطلب أو تحليل البيانات ، فإننا ننتقل إلى حالة "خطأ".
- نجاح
- خطأ
نعرض رسالة خطأ ونعرض زر جلب البيانات. تقبل هذه الحالة إجراءً واحدًا:- أعد المحاولة
عندما ينقر المستخدم على زر إعادة المحاولة ، فإننا نطلق الطلب مرة أخرى وننقل الجهاز إلى حالة "الجلب".
- أعد المحاولة
لقد وصفنا نفس العمليات تقريبًا ، ولكن مع الحالات والمدخلات.
هذا يبسط المنطق ويجعله أكثر قابلية للتنبؤ. كما أنه يحل بعض المشاكل المذكورة أعلاه. لاحظ أنه بينما نحن في حالة "الجلب" ، فإننا لا نقبل أي نقرات. لذلك ، حتى إذا نقر المستخدم على الزر ، فلن يحدث شيء لأن الجهاز غير مهيأ للاستجابة لهذا الإجراء أثناء وجوده في هذه الحالة. يزيل هذا الأسلوب تلقائيًا التفرع غير المتوقع لمنطق الكود الخاص بنا. هذا يعني أنه سيكون لدينا عدد أقل من التعليمات البرمجية التي يجب تغطيتها أثناء الاختبار . أيضًا ، يمكن أتمتة بعض أنواع الاختبارات ، مثل اختبار التكامل. فكر في كيفية الحصول على فكرة واضحة حقًا عما يفعله تطبيقنا ، ويمكننا إنشاء نص برمجي يتخطى الحالات المحددة والانتقالات ويولد التأكيدات. يمكن أن تثبت هذه التأكيدات أننا وصلنا إلى كل حالة ممكنة أو قمنا بتغطية رحلة معينة.
في الحقيقة ، تدوين كل الحالات الممكنة أسهل من كتابة كل التحولات الممكنة لأننا نعرف الحالات التي نحتاجها أو لدينا. بالمناسبة ، في معظم الحالات ، تصف الدول منطق الأعمال لتطبيقنا ، في حين أن الانتقالات غالبًا ما تكون غير معروفة في البداية. الأخطاء الموجودة في برنامجنا هي نتيجة للإجراءات التي تم إرسالها في حالة خاطئة و / أو في الوقت الخطأ. يتركون تطبيقنا في حالة لا نعرف عنها شيئًا ، وهذا يكسر برنامجنا أو يجعله يتصرف بشكل غير صحيح. بالطبع ، لا نريد أن نكون في مثل هذا الموقف. آلات الدولة هي جدران حماية جيدة . إنها تحمينا من الوصول إلى حالات مجهولة لأننا وضعنا حدودًا لما يمكن أن يحدث ومتى ، دون أن نقول صراحةً كيف. يقترن مفهوم آلة الحالة بشكل جيد مع تدفق بيانات أحادي الاتجاه. يعملان معًا على تقليل تعقيد الكود وإلغاء غموض المكان الذي نشأت فيه الحالة.
إنشاء آلة دولة في JavaScript
يكفي الحديث - دعنا نرى بعض التعليمات البرمجية. سوف نستخدم نفس المثال. بناءً على القائمة أعلاه ، سنبدأ بما يلي:
const machine = { 'idle': { click: function () { ... } }, 'fetching': { success: function () { ... }, failure: function () { ... } }, 'error': { 'retry': function () { ... } } }
لدينا الحالات ككائنات ومدخلاتها الممكنة كوظائف. الحالة الأولية مفقودة ، رغم ذلك. دعنا نغير الكود أعلاه إلى هذا:
const machine = { state: 'idle', transitions: { 'idle': { click: function() { ... } }, 'fetching': { success: function() { ... }, failure: function() { ... } }, 'error': { 'retry': function() { ... } } } }
بمجرد أن نحدد جميع الحالات التي تبدو منطقية بالنسبة لنا ، نكون مستعدين لإرسال المدخلات وتغيير الحالة. سنفعل ذلك باستخدام الطريقتين المساعدتين أدناه:
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
مما إذا كان هناك إجراء بالاسم المحدد في انتقالات الحالة الحالية. إذا كان الأمر كذلك ، فإنه يطلقها بالحمولة المحددة. نحن أيضًا نسمي معالج action
مع machine
كسياق ، حتى نتمكن من إرسال إجراءات أخرى باستخدام this.dispatch(<action>)
أو تغيير الحالة باستخدام this.changeStateTo(<new state>)
.
باتباع رحلة المستخدم في مثالنا ، فإن أول إجراء يتعين علينا إرساله هو click
. إليك ما يبدو عليه معالج هذا الإجراء:
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');
نقوم أولاً بتغيير حالة الآلة إلى حالة fetching
. ثم نقوم بتشغيل الطلب إلى النهاية الخلفية. لنفترض أن لدينا خدمة بطريقة getData
ترجع وعدًا. بمجرد أن يتم حلها ويكون تحليل البيانات على ما يرام ، فإننا نرسل success
، إن لم يكن failure
.
حتى الان جيدة جدا. بعد ذلك ، يتعين علينا تنفيذ إجراءات ومدخلات success
failure
في حالة fetching
:
transitions: { 'idle': { ... }, 'fetching': { success: function (data) { // render the data this.changeStateTo('idle'); }, failure: function (error) { this.changeStateTo('error'); } }, ... }
لاحظ كيف حررنا عقولنا من الاضطرار إلى التفكير في العملية السابقة. نحن لا نهتم بنقرات المستخدم أو ما يحدث مع طلب HTTP. نحن نعلم أن التطبيق في حالة fetching
، ونتوقع هذين الإجراءين فقط. إنه يشبه إلى حد ما كتابة منطق جديد بمعزل عن غيره.
آخر بت هو حالة error
. سيكون من الرائع لو قدمنا منطق إعادة المحاولة هذا حتى يتمكن التطبيق من التعافي من الفشل.
transitions: { 'error': { retry: function () { this.changeStateTo('idle'); this.dispatch('click'); } } }
هنا يتعين علينا تكرار المنطق الذي كتبناه في معالج click
. لتجنب ذلك ، يجب علينا إما تحديد المعالج كوظيفة يمكن الوصول إليها لكلا الإجراءين ، أو ننتقل أولاً إلى حالة idle
ثم نرسل إجراء click
يدويًا.
يمكن العثور على مثال كامل لآلة حالة العمل في Codepen الخاص بي.
إدارة أجهزة الدولة بمكتبة
يعمل نمط آلة الحالة المحدودة بغض النظر عما إذا كنا نستخدم React أو Vue أو Angular. كما رأينا في القسم السابق ، يمكننا بسهولة تنفيذ آلة الحالة دون الكثير من المتاعب. ومع ذلك ، توفر المكتبة في بعض الأحيان مزيدًا من المرونة. بعض من الأشياء الجيدة هي Machina.js و XState. ومع ذلك ، سنتحدث في هذه المقالة عن Stent ، مكتبتي الشبيهة بـ Redux التي تخبز في مفهوم آلات الحالة المحدودة.
Stent هو تنفيذ حاوية آلات الحالة. إنه يتبع بعض الأفكار في مشاريع Redux و Redux-Saga ، لكنه يوفر ، في رأيي ، عمليات أبسط وخالية من المتغيرات. تم تطويره باستخدام التطوير المستند إلى الملف التمهيدي ، وقد أمضيت أسابيع فقط في تصميم واجهة برمجة التطبيقات. نظرًا لأنني كنت أكتب المكتبة ، فقد أتيحت لي الفرصة لإصلاح المشكلات التي واجهتها أثناء استخدام بنيات Redux و Flux.
صنع الآلات
في معظم الحالات ، تغطي تطبيقاتنا مجالات متعددة. لا يمكننا استخدام آلة واحدة فقط. لذلك ، يسمح Stent بإنشاء العديد من الأجهزة:
import { Machine } from 'stent'; const machineA = Machine.create('A', { state: ..., transitions: ... }); const machineB = Machine.create('B', { state: ..., transitions: ... });
لاحقًا ، يمكننا الوصول إلى هذه الأجهزة باستخدام طريقة Machine.get
:
const machineA = Machine.get('A'); const machineB = Machine.get('B');
ربط الآلات بمنطق التقديم
يتم إجراء التقديم في حالتي عبر React ، لكن يمكننا استخدام أي مكتبة أخرى. يتلخص الأمر في إطلاق رد نداء حيث نقوم بتشغيل العرض. كانت إحدى الميزات الأولى التي عملت عليها هي وظيفة connect
:
import { connect } from 'stent/lib/helpers'; Machine.create('MachineA', ...); Machine.create('MachineB', ...); connect() .with('MachineA', 'MachineB') .map((MachineA, MachineB) => { ... rendering here });
نقول ما هي الآلات المهمة بالنسبة لنا ونعطي أسمائها. يتم تشغيل رد الاتصال الذي نمرره إلى map
مرة واحدة في البداية ثم لاحقًا في كل مرة تتغير حالة بعض الأجهزة. هذا هو المكان الذي نشغل فيه العرض. في هذه المرحلة ، لدينا وصول مباشر إلى الأجهزة المتصلة ، حتى نتمكن من استرداد الحالة والطرق الحالية. هناك أيضًا mapOnce
، لتشغيل رد الاتصال مرة واحدة فقط ، و mapSilent
، لتخطي هذا التنفيذ الأولي.
للراحة ، يتم تصدير المساعد خصيصًا لتكامل React. إنه مشابه حقًا 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 بتشغيل رد اتصال التعيين الخاص بنا ويتوقع استلام كائن - كائن يتم إرساله كدعامات إلى مكون props
الخاص بنا.
ما هي الدولة في سياق الدعامة؟
حتى الآن ، كانت دولتنا مجرد أوتار. لسوء الحظ ، في العالم الحقيقي ، علينا أن نحتفظ بأكثر من سلسلة في الحالة. هذا هو السبب في أن حالة Stent هي في الواقع كائن بداخله خصائص. الخاصية الوحيدة المحجوزة هي name
. كل شيء آخر هو بيانات خاصة بالتطبيق. علي سبيل المثال:
{ name: 'idle' } { name: 'fetching', todos: [] } { name: 'forward', speed: 120, gear: 4 }
تُظهر لي تجربتي مع Stent حتى الآن أنه إذا أصبح كائن الحالة أكبر ، فربما نحتاج إلى آلة أخرى تتعامل مع تلك الخصائص الإضافية. يستغرق تحديد الحالات المختلفة بعض الوقت ، لكنني أعتقد أن هذه خطوة كبيرة إلى الأمام في كتابة تطبيقات أكثر قابلية للإدارة. إنه يشبه إلى حد ما التنبؤ بالمستقبل ورسم أطر الإجراءات المحتملة.
العمل مع آلة الدولة
على غرار المثال الوارد في البداية ، يتعين علينا تحديد الحالات (المحدودة) المحتملة لجهازنا ووصف المدخلات المحتملة:
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' }; } } } });
لدينا حالتنا الأولية ، idle
، التي تقبل إجراء run
. بمجرد أن تكون الآلة في حالة running
، يمكننا إطلاق إجراء stop
، والذي يعيدنا إلى حالة idle
.
من المحتمل أن تتذكر dispatch
و changeStateTo
المساعدين من تطبيقنا في وقت سابق. توفر هذه المكتبة نفس المنطق ، لكنها مخفية داخليًا ، ولا يتعين علينا التفكير فيها. للراحة ، بناءً على خاصية transitions
، تقوم Stent بإنشاء ما يلي:
- الطرق المساعدة للتحقق مما إذا كانت الآلة في حالة معينة - تنتج حالة
idle
طريقةisIdle()
، بينما عندrunning
لديناisRunning()
؛ - التوابع المساعدة لإرسال الإجراءات:
runPlease()
وstopNow()
.
لذلك ، في المثال أعلاه ، يمكننا استخدام هذا:
machine.isIdle(); // boolean machine.isRunning(); // boolean machine.runPlease(); // fires action machine.stopNow(); // fires action
بدمج الطرق التي تم إنشاؤها تلقائيًا مع وظيفة أداة connect
، يمكننا إغلاق الدائرة. يؤدي تفاعل المستخدم إلى تشغيل إدخال الجهاز وعمله ، مما يؤدي إلى تحديث الحالة. بسبب هذا التحديث ، يتم تشغيل وظيفة التعيين التي تم تمريرها connect
، ويتم إبلاغنا بتغيير الحالة. ثم نقوم بإعادة العرض.
معالجات الإدخال والإجراء
ربما يكون الشيء الأكثر أهمية هو معالجات العمل. هذا هو المكان الذي نكتب فيه معظم منطق التطبيق لأننا نستجيب للإدخال والحالات المتغيرة. تم أيضًا دمج شيء أحبه حقًا في Redux هنا: ثبات وظيفة المخفض وبساطتها. جوهر معالج العمل في Stent هو نفسه. يتلقى الحالة الحالية وحمولة الإجراء ، ويجب أن يعيد الحالة الجديدة. إذا لم يقم المعالج بإرجاع أي شيء ( undefined
) ، فإن حالة الجهاز تظل كما هي.
transitions: { 'fetching': { 'success': function (state, payload) { const todos = [ ...state.todos, payload ]; return { name: 'idle', todos }; } } }
لنفترض أننا بحاجة إلى جلب البيانات من خادم بعيد. نقوم بإطلاق الطلب ونقل الآلة إلى حالة fetching
. بمجرد أن تأتي البيانات من النهاية الخلفية ، فإننا نطلق إجراءً success
، مثل:
machine.success({ label: '...' });
ثم نعود إلى حالة idle
ونحتفظ ببعض البيانات في شكل مجموعة todos
. هناك بضع قيم أخرى محتملة لتعيينها كمعالجات إجراءات. الحالة الأولى والأبسط هي عندما نمرر سلسلة نصية تصبح الحالة الجديدة.
transitions: { 'idle': { 'run': 'running' } }
هذا انتقال من { name: 'idle' }
إلى { name: 'running' }
باستخدام الإجراء run()
. هذا الأسلوب مفيد عندما يكون لدينا انتقالات حالة متزامنة وليس لدينا أي بيانات تعريف. لذا ، إذا احتفظنا بشيء آخر في حالة ، فإن هذا النوع من الانتقال سيطرده. وبالمثل ، يمكننا تمرير كائن الحالة مباشرة:
transitions: { 'editing': { 'delete all todos': { name: 'idle', todos: [] } } }
نحن ننتقل من editing
إلى idle
باستخدام الإجراء deleteAllTodos
.
لقد رأينا بالفعل معالج الوظيفة ، وآخر متغير لمعالج الإجراء هو وظيفة المولد. إنه مستوحى من مشروع Redux-Saga ، ويبدو كالتالي:
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 }; } } } });
إذا لم تكن لديك خبرة في المولدات ، فقد يبدو هذا غامضًا بعض الشيء. لكن المولدات في JavaScript هي أداة قوية. يُسمح لنا بإيقاف معالج الإجراءات مؤقتًا وتغيير الحالة عدة مرات والتعامل مع منطق غير متزامن.
المرح مع المولدات
عندما تعرفت على Redux-Saga لأول مرة ، اعتقدت أنها طريقة معقدة للغاية للتعامل مع العمليات غير المتزامنة. في الواقع ، إنه تطبيق ذكي جدًا لنمط تصميم الأوامر. الفائدة الرئيسية من هذا النمط هي أنه يفصل بين استدعاء المنطق وتنفيذه الفعلي.
بعبارة أخرى ، نقول ما نريد ولكن لا نقول كيف يجب أن يحدث. ساعدتني سلسلة مدونة Matt Hink في فهم كيفية تنفيذ الملاحم ، وأنا أوصي بشدة بقراءتها. لقد أحضرت نفس الأفكار إلى Stent ، ولغرض هذه المقالة ، سنقول أنه من خلال تقديم الأشياء ، فإننا نعطي تعليمات حول ما نريده دون فعل ذلك في الواقع. بمجرد تنفيذ الإجراء ، نتلقى التحكم مرة أخرى.
في الوقت الحالي ، قد يتم إرسال أمرين (تسفر عنهما):
- كائن حالة (أو سلسلة) لتغيير حالة الجهاز ؛
-
call
مساعد الاتصال (يقبل وظيفة متزامنة ، وهي وظيفة تُعيد وعدًا أو وظيفة مولد أخرى) - نحن نقول أساسًا ، "قم بتشغيل هذا من أجلي ، وإذا كان غير متزامن ، فانتظر. بمجرد الانتهاء ، أعطني النتيجة. "؛ - استدعاء مساعد
wait
(يقبل سلسلة تمثل إجراءً آخر) ؛ إذا استخدمنا وظيفة الأداة المساعدة هذه ، فإننا نوقف المعالج مؤقتًا وننتظر إرسال إجراء آخر.
فيما يلي وظيفة توضح المتغيرات:
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 }; } } }
كما نرى ، يبدو الرمز متزامنًا ، لكنه في الحقيقة ليس كذلك. إنها مجرد Stent تقوم بالجزء الممل المتمثل في انتظار الوعد الذي تم حله أو التكرار على مولد آخر.
كيف تعمل الدعامات على حل مخاوفي بشأن إعادة الإرسال
الكثير من كود Boilerplate
تعتمد بنية Redux (و Flux) على الإجراءات التي يتم تداولها في نظامنا. عندما ينمو التطبيق ، ينتهي بنا الأمر عادةً إلى امتلاك الكثير من الثوابت ومنشئي الإجراءات. غالبًا ما يكون هذان الشيئان في مجلدات مختلفة ، وقد يستغرق تتبع تنفيذ الكود بعض الوقت في بعض الأحيان. أيضًا ، عند إضافة ميزة جديدة ، يتعين علينا دائمًا التعامل مع مجموعة كاملة من الإجراءات ، مما يعني تحديد المزيد من أسماء الإجراءات ومنشئي الإجراءات.
في Stent ، ليس لدينا أسماء إجراءات ، وتقوم المكتبة بإنشاء منشئ الإجراء تلقائيًا لنا:
const machine = Machine.create('todo-app', { state: { name: 'idle', todos: [] }, transitions: { 'idle': { 'add todo': function (state, todo) { ... } } } }); machine.addTodo({ title: 'Fix that bug' });
لدينا منشئ الإجراءات machine.addTodo
الذي تم تعريفه مباشرة على أنه طريقة للآلة. حل هذا النهج أيضًا مشكلة أخرى واجهتها: العثور على المخفض الذي يستجيب لعمل معين. عادة ، في مكونات React ، نرى أسماء منشئ الإجراء مثل addTodo
؛ ومع ذلك ، في المخفضات ، نعمل بنوع من الإجراء الثابت. في بعض الأحيان ، يتعين علي الانتقال إلى رمز منشئ الإجراء فقط حتى أتمكن من رؤية النوع الدقيق. هنا ، ليس لدينا أنواع على الإطلاق.
تغييرات حالة غير متوقعة
بشكل عام ، تقوم Redux بعمل جيد في إدارة الحالة بطريقة ثابتة. المشكلة ليست في Redux نفسها ، ولكن في ذلك يُسمح للمطور بإرسال أي إجراء في أي وقت. إذا قلنا أن لدينا إجراء يضيء الأضواء ، فهل من المقبول إطلاق هذا الإجراء مرتين على التوالي؟ إذا لم يكن الأمر كذلك ، فكيف من المفترض أن نحل هذه المشكلة مع Redux؟ حسنًا ، من المحتمل أن نضع بعض الكود في المخفض الذي يحمي المنطق ويتحقق مما إذا كانت الأضواء مضاءة بالفعل - ربما عبارة if
التي تتحقق من الحالة الحالية. الآن السؤال هو ، أليس هذا خارج نطاق المخفض؟ هل يجب أن يعرف المخفض عن حالات الحافة هذه؟
ما أفتقده في Redux هو طريقة لإيقاف إرسال إجراء بناءً على الحالة الحالية للتطبيق دون تلويث المخفض بالمنطق الشرطي. ولا أريد أن أتخذ هذا القرار إلى طبقة العرض أيضًا ، حيث يتم تشغيل منشئ الإجراء. مع Stent ، يحدث هذا تلقائيًا لأن الجهاز لا يستجيب للإجراءات التي لم يتم التصريح عنها في الحالة الحالية. علي سبيل المثال:
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();
حقيقة أن الجهاز يقبل مدخلات محددة فقط في وقت معين تحمينا من الأخطاء الغريبة وتجعل تطبيقاتنا أكثر قابلية للتنبؤ.
الدول وليس التحولات
إعادة ، مثل Flux ، تجعلنا نفكر من حيث التحولات. إن النموذج العقلي للتطوير باستخدام Redux مدفوع إلى حد كبير بالأفعال وكيف تحول هذه الإجراءات الحالة في مخفضاتنا. هذا ليس سيئًا ، لكنني وجدت أنه من المنطقي التفكير فيما يتعلق بالحالات بدلاً من ذلك - ما هي الحالات التي قد يكون التطبيق فيها وكيف تمثل هذه الحالات متطلبات العمل.
خاتمة
كان مفهوم آلات الدولة في البرمجة ، وخاصة في تطوير واجهة المستخدم ، أمرًا لافتًا للنظر بالنسبة لي. بدأت أرى آلات الدولة في كل مكان ، ولدي بعض الرغبة في التحول دائمًا إلى هذا النموذج. أرى بالتأكيد فوائد وجود حالات وانتقالات محددة بدقة أكبر فيما بينها. أنا أبحث دائمًا عن طرق لجعل تطبيقاتي بسيطة وقابلة للقراءة. أعتقد أن آلات الدولة هي خطوة في هذا الاتجاه. المفهوم بسيط وقوي في نفس الوقت. لديها القدرة على القضاء على الكثير من الأخطاء.