การเพิ่มขึ้นของเครื่องจักรของรัฐ
เผยแพร่แล้ว: 2022-03-10ผ่านไปแล้วในปี 2018 และนักพัฒนาส่วนหน้าอีกนับไม่ถ้วนยังคงต่อสู้กับความซับซ้อนและความไม่สามารถเคลื่อนไหวได้ เดือนแล้วเดือนเล่า พวกเขาได้ค้นหาจอกศักดิ์สิทธิ์: สถาปัตยกรรมแอปพลิเคชันที่ปราศจากข้อบกพร่อง ที่จะช่วยให้ส่งมอบได้อย่างรวดเร็วและมีคุณภาพสูง ฉันเป็นหนึ่งในนักพัฒนาเหล่านั้น และพบสิ่งที่น่าสนใจที่อาจช่วยได้
เราได้ก้าวไปข้างหน้าด้วยเครื่องมือต่างๆ เช่น React และ Redux อย่างไรก็ตาม มันยังไม่เพียงพอสำหรับการใช้งานขนาดใหญ่ บทความนี้จะแนะนำแนวคิดของเครื่องจักรของรัฐในบริบทของการพัฒนาส่วนหน้า คุณคงสร้างมันขึ้นมาหลายตัวแล้วโดยไม่รู้ตัว
บทนำสู่สถานะเครื่อง
เครื่องสถานะเป็นแบบจำลองทางคณิตศาสตร์ของการคำนวณ เป็นแนวคิดที่เป็นนามธรรมโดยที่เครื่องสามารถมีสถานะต่างๆ กันได้ แต่ในช่วงเวลาที่กำหนดจะมีเพียงสถานะเดียวเท่านั้น มีเครื่องจักรของรัฐหลายประเภท ฉันเชื่อว่าเครื่องที่มีชื่อเสียงที่สุดคือเครื่องทัวริง เป็นเครื่องสถานะอนันต์ ซึ่งหมายความว่าสามารถมีสถานะจำนวนนับไม่ถ้วน เครื่องทัวริงไม่เหมาะกับการพัฒนา UI ในปัจจุบัน เนื่องจากในกรณีส่วนใหญ่ เรามีสถานะจำนวนจำกัด นี่คือเหตุผลที่ว่าทำไมเครื่องจักรที่มีสถานะจำกัด เช่น Mealy และ Moore จึงเหมาะสมกว่า
ความแตกต่างระหว่างพวกเขาคือเครื่องมัวร์เปลี่ยนสถานะตามสถานะก่อนหน้าเท่านั้น น่าเสียดายที่เรามีปัจจัยภายนอกหลายอย่าง เช่น การโต้ตอบกับผู้ใช้และกระบวนการเครือข่าย ซึ่งหมายความว่าเครื่องของ Moore ก็ไม่ดีพอสำหรับเราเช่นกัน สิ่งที่เรากำลังมองหาคือเครื่อง Mealy มีสถานะเริ่มต้นแล้วเปลี่ยนเป็นสถานะใหม่ตามข้อมูลเข้าและสถานะปัจจุบัน
วิธีที่ง่ายที่สุดวิธีหนึ่งในการแสดงให้เห็นว่าเครื่องของรัฐทำงานอย่างไรคือการดูประตูหมุน มันมีสถานะจำนวนจำกัด: ล็อคและปลดล็อค นี่คือกราฟิกง่ายๆ ที่แสดงให้เราเห็นสถานะเหล่านี้ พร้อมอินพุตและทรานซิชันที่เป็นไปได้
สถานะเริ่มต้นของประตูหมุนถูกล็อค ไม่ว่าเราจะกดกี่ครั้ง มันก็อยู่ในสถานะล็อคนั้น อย่างไรก็ตาม หากเราส่งเหรียญไปให้ เหรียญนั้นก็จะเปลี่ยนเป็นสถานะปลดล็อค เหรียญอื่น ณ จุดนี้จะไม่ทำอะไรเลย มันจะยังอยู่ในสถานะปลดล็อค แรงผลักดันจากอีกฝั่งจะได้ผล และเราจะผ่านพ้นไปได้ การดำเนินการนี้ยังเปลี่ยนเครื่องเป็นสถานะล็อกเริ่มต้นอีกด้วย
หากเราต้องการใช้ฟังก์ชันเดียวที่ควบคุม turnstile เราอาจจะจบลงด้วยสองอาร์กิวเมนต์: สถานะปัจจุบันและการดำเนินการ และถ้าคุณใช้ Redux สิ่งนี้อาจฟังดูคุ้นเคยสำหรับคุณ คล้ายกับฟังก์ชันรีดิวเซอร์ที่รู้จักกันดี ซึ่งเราได้รับสถานะปัจจุบัน และขึ้นอยู่กับเพย์โหลดของการดำเนินการ เราจะตัดสินใจว่าสถานะถัดไปจะเป็นอย่างไร ตัวลดคือการเปลี่ยนแปลงในบริบทของเครื่องของรัฐ อันที่จริง แอปพลิเคชั่นใด ๆ ที่มีสถานะที่เราเปลี่ยนแปลงได้อาจเรียกได้ว่าเป็นเครื่องของรัฐ เป็นเพียงว่าเรากำลังดำเนินการทุกอย่างด้วยตนเองครั้งแล้วครั้งเล่า
เครื่องของรัฐดีกว่าอย่างไร?
ที่ทำงาน เราใช้ Redux และฉันค่อนข้างพอใจกับมัน อย่างไรก็ตาม ฉันเริ่มเห็นรูปแบบที่ฉันไม่ชอบ โดย “ไม่ชอบ” ฉันไม่ได้หมายความว่าพวกเขาไม่ทำงาน มันเป็นมากกว่าที่พวกเขาเพิ่มความซับซ้อนและบังคับให้ฉันเขียนโค้ดเพิ่มเติม ฉันต้องทำโปรเจ็กต์เสริมที่ฉันมีที่ว่างให้ทดลอง และฉันตัดสินใจที่จะคิดใหม่เกี่ยวกับแนวทางปฏิบัติในการพัฒนา React และ Redux ฉันเริ่มจดบันทึกเกี่ยวกับสิ่งต่าง ๆ ที่ทำให้ฉันกังวล และฉันก็ตระหนักว่าสภาวะที่เป็นนามธรรมของเครื่องจักรจะช่วยแก้ปัญหาเหล่านี้ได้จริงๆ มาดูวิธีการติดตั้ง state machine ใน JavaScript กัน
เราจะโจมตีปัญหาง่ายๆ เราต้องการดึงข้อมูลจาก back-end API และแสดงให้ผู้ใช้เห็น ขั้นตอนแรกสุดคือการเรียนรู้วิธีการคิดในสถานะต่างๆ มากกว่าการเปลี่ยนผ่าน ก่อนที่เราจะเข้าสู่ state machine เวิร์กโฟลว์ของฉันสำหรับการสร้างคุณสมบัติดังกล่าวเคยมีลักษณะดังนี้:
- เราแสดงปุ่มดึงข้อมูล
- ผู้ใช้คลิกปุ่มดึงข้อมูล
- ส่งคำขอไปที่ส่วนหลัง
- ดึงข้อมูลและแยกวิเคราะห์
- แสดงให้ผู้ใช้เห็น
- หรือหากมีข้อผิดพลาด ให้แสดงข้อความแสดงข้อผิดพลาดและแสดงปุ่มดึงข้อมูล เพื่อให้เราสามารถทริกเกอร์กระบวนการได้อีกครั้ง
เรากำลังคิดแบบเส้นตรงและโดยพื้นฐานแล้วพยายามที่จะครอบคลุมทิศทางที่เป็นไปได้ทั้งหมดไปยังผลลัพธ์สุดท้าย ขั้นตอนหนึ่งนำไปสู่อีกขั้นตอนหนึ่ง และเราจะเริ่มแยกรหัสของเราอย่างรวดเร็ว สิ่งที่เกี่ยวกับปัญหาเช่นผู้ใช้ดับเบิลคลิกที่ปุ่ม หรือผู้ใช้คลิกที่ปุ่มในขณะที่เรากำลังรอการตอบสนองของแบ็กเอนด์ หรือคำขอสำเร็จแต่ข้อมูลเสียหาย ในกรณีเหล่านี้ เราอาจจะมีแฟล็กต่างๆ ที่แสดงให้เราเห็นว่าเกิดอะไรขึ้น การมีแฟล็กหมายถึงส่วนคำสั่ง if
มากขึ้น และในแอพที่ซับซ้อนมากขึ้น ความขัดแย้งที่มากขึ้น
นี่เป็นเพราะว่าเรากำลังคิดอยู่ในช่วงเปลี่ยนผ่าน เรากำลังมุ่งเน้นไปที่การเปลี่ยนแปลงที่เกิดขึ้นและในลำดับใด การมุ่งเน้นไปที่สถานะต่างๆ ของแอปพลิเคชันแทนจะง่ายกว่ามาก เรามีกี่สถานะ และอะไรคือปัจจัยการผลิตที่เป็นไปได้ ใช้ตัวอย่างเดียวกัน:
- ว่าง
ในสถานะนี้ เราจะแสดงปุ่มดึงข้อมูล นั่งรอ การดำเนินการที่เป็นไปได้คือ:- คลิก
เมื่อผู้ใช้คลิกปุ่ม เรากำลังส่งคำขอไปยังส่วนหลัง จากนั้นเปลี่ยนเครื่องเป็นสถานะ "กำลังดึงข้อมูล"
- คลิก
- กำลังเรียก
คำขอกำลังบินและเรานั่งรอ การกระทำคือ:- ความสำเร็จ
ข้อมูลมาถึงสำเร็จและไม่เสียหาย เราใช้ข้อมูลในทางใดทางหนึ่งและเปลี่ยนกลับเป็นสถานะ "ว่าง" - ความล้มเหลว
หากมีข้อผิดพลาดขณะส่งคำขอหรือแยกวิเคราะห์ข้อมูล เราจะเปลี่ยนเป็นสถานะ "ข้อผิดพลาด"
- ความสำเร็จ
- ข้อผิดพลาด
เราแสดงข้อความแสดงข้อผิดพลาดและแสดงปุ่มดึงข้อมูล สถานะนี้ยอมรับหนึ่งการกระทำ:- ลองอีกครั้ง
เมื่อผู้ใช้คลิกปุ่มลองใหม่ เราจะเริ่มคำขออีกครั้งและเปลี่ยนเครื่องเป็นสถานะ "กำลังดึงข้อมูล"
- ลองอีกครั้ง
เราได้อธิบายคร่าวๆ เกี่ยวกับกระบวนการเดียวกัน แต่มีสถานะและอินพุต
วิธีนี้ช่วยลดความซับซ้อนของตรรกะและทำให้คาดเดาได้มากขึ้น นอกจากนี้ยังแก้ปัญหาบางอย่างที่กล่าวถึงข้างต้น โปรดสังเกตว่าในขณะที่เราอยู่ในสถานะ "กำลังดึงข้อมูล" เราไม่ยอมรับการคลิกใดๆ ดังนั้น แม้ว่าผู้ใช้จะคลิกปุ่ม จะไม่มีอะไรเกิดขึ้น เนื่องจากเครื่องไม่ได้กำหนดค่าให้ตอบสนองต่อการกระทำนั้นในขณะที่อยู่ในสถานะนั้น วิธีการนี้จะขจัดการแตกสาขาที่คาดเดาไม่ได้ของตรรกะโค้ดของเราโดยอัตโนมัติ ซึ่งหมายความว่าเราจะมี โค้ดน้อยกว่าที่จะครอบคลุมขณะทำการทดสอบ นอกจากนี้ การทดสอบบางประเภท เช่น การทดสอบการรวม สามารถทำแบบอัตโนมัติได้ ลองนึกดูว่าเราจะมีแนวคิดที่ชัดเจนจริงๆ ว่าแอปพลิเคชันของเราทำอะไรได้บ้าง และเราสามารถสร้างสคริปต์ที่ครอบคลุมสถานะและช่วงการเปลี่ยนภาพที่กำหนดไว้ และสร้างการยืนยันได้ การยืนยันเหล่านี้สามารถพิสูจน์ว่าเราได้มาถึงทุกสถานะที่เป็นไปได้หรือครอบคลุมการเดินทางเฉพาะ
ที่จริงแล้ว การเขียนสถานะที่เป็นไปได้ทั้งหมดนั้นง่ายกว่าการเขียนการเปลี่ยนผ่านที่เป็นไปได้ทั้งหมด เพราะเรารู้ว่ารัฐใดที่เราต้องการหรือมี ในกรณีส่วนใหญ่ รัฐจะอธิบายตรรกะทางธุรกิจของแอปพลิเคชันของเรา ในขณะที่การเปลี่ยนมักจะไม่เป็นที่รู้จักในตอนเริ่มต้น ข้อบกพร่องในซอฟต์แวร์ของเราเป็นผลมาจากการดำเนินการที่ส่งไปในสถานะที่ไม่ถูกต้องและ/หรือในเวลาที่ไม่ถูกต้อง พวกเขาปล่อยให้แอปของเราอยู่ในสถานะที่เราไม่รู้ และทำให้โปรแกรมของเราหยุดชะงักหรือทำให้แอปทำงานไม่ถูกต้อง แน่นอน เราไม่ต้องการที่จะอยู่ในสถานการณ์ดังกล่าว เครื่องของรัฐเป็นไฟร์วอลล์ที่ดี พวกเขาปกป้องเราจากการไปถึงรัฐที่ไม่รู้จักเพราะเรากำหนดขอบเขตสำหรับสิ่งที่สามารถเกิดขึ้นได้และเมื่อใด โดยไม่บอกอย่างชัดเจนว่าจะทำอย่างไร แนวคิดของ State Machine เข้ากันได้ดีกับการไหลของข้อมูลแบบทิศทางเดียว ร่วมกันช่วยลดความซับซ้อนของรหัสและไขปริศนาว่ารัฐมีต้นกำเนิดมาจากที่ใด
การสร้างเครื่องสถานะใน 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 ตามที่เราเห็นในหัวข้อก่อนหน้านี้ เราสามารถใช้ state machine ได้อย่างง่ายดายโดยไม่มีปัญหาอะไรมาก อย่างไรก็ตาม บางครั้งห้องสมุดก็มีความยืดหยุ่นมากกว่า สิ่งที่ดีบางอย่างคือ Machina.js และ XState อย่างไรก็ตาม ในบทความนี้ เราจะพูดถึง Stent ไลบรารีที่เหมือน Redux ของฉัน ซึ่งใช้แนวคิดของเครื่อง finite state
Stent เป็นการนำตู้คอนเทนเนอร์ของรัฐไปใช้ มันเป็นไปตามแนวคิดบางอย่างในโครงการ Redux และ Redux-Saga แต่ในความคิดของฉันมีกระบวนการที่ง่ายกว่าและไม่ต้องใช้ต้นแบบ ได้รับการพัฒนาโดยใช้การพัฒนาที่ขับเคลื่อนด้วย readme และฉันใช้เวลาหลายสัปดาห์กับการออกแบบ API เท่านั้น เนื่องจากฉันกำลังเขียนไลบรารี ฉันจึงมีโอกาสแก้ไขปัญหาที่ฉันพบขณะใช้สถาปัตยกรรม 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
ไปยังองค์ประกอบ React ของเรา
รัฐในบริบทของ Stent คืออะไร?
จวบจนปัจจุบัน สถานะของเราเป็นเพียงแค่สตริงธรรมดาๆ น่าเสียดาย ในโลกแห่งความเป็นจริง เราต้องรักษาสถานะให้มากกว่าสตริง นี่คือเหตุผลที่สถานะของ Stent เป็นวัตถุที่มีคุณสมบัติอยู่ภายใน ทรัพย์สินที่สงวนไว้แห่งเดียวคือ name
อย่างอื่นเป็นข้อมูลเฉพาะแอป ตัวอย่างเช่น:
{ name: 'idle' } { name: 'fetching', todos: [] } { name: 'forward', speed: 120, gear: 4 }
ประสบการณ์ของฉันกับ Stent จนถึงตอนนี้แสดงให้ฉันเห็นว่าหาก state object มีขนาดใหญ่ขึ้น เราอาจต้องการเครื่องอื่นที่จัดการคุณสมบัติเพิ่มเติมเหล่านั้น การระบุสถานะต่างๆ อาจต้องใช้เวลา แต่ฉันเชื่อว่านี่เป็นก้าวสำคัญในการเขียนแอปพลิเคชันที่สามารถจัดการได้มากขึ้น มันเหมือนกับการทำนายอนาคตและการวาดกรอบของการกระทำที่เป็นไปได้เล็กน้อย
การทำงานกับเครื่องสถานะ
คล้ายกับตัวอย่างในตอนเริ่มต้น เราต้องกำหนดสถานะที่เป็นไปได้ (จำกัด) ของเครื่องของเราและอธิบายอินพุตที่เป็นไปได้:
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()
วิธีนี้มีประโยชน์เมื่อเรามีการเปลี่ยนสถานะแบบซิงโครนัสและไม่มีข้อมูลเมตา ดังนั้น หากเรารักษาอย่างอื่นไว้ในสถานะ การเปลี่ยนแปลงประเภทนั้นก็จะล้างออก ในทำนองเดียวกัน เราสามารถส่ง state object ได้โดยตรง:
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 เป็นครั้งแรก ฉันคิดว่านี่เป็นวิธีที่ซับซ้อนเกินไปในการจัดการการดำเนินการแบบ async อันที่จริงมันเป็นการนำรูปแบบการออกแบบคำสั่งไปใช้อย่างชาญฉลาด ประโยชน์หลักของรูปแบบนี้คือแยกการเรียกใช้ตรรกะและการนำไปใช้จริง
กล่าวอีกนัยหนึ่งเราพูดในสิ่งที่เราต้องการแต่ไม่ใช่วิธีที่มันควรจะเกิดขึ้น ชุดบล็อกของ 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 ช่วยแก้ปัญหา Redux ของฉันได้อย่างไร
รหัส 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();
ความจริงที่ว่าเครื่องยอมรับเฉพาะอินพุตในช่วงเวลาที่กำหนด ช่วยปกป้องเราจากข้อบกพร่องแปลก ๆ และทำให้แอปพลิเคชันของเราสามารถคาดเดาได้มากขึ้น
รัฐ ไม่ใช่การเปลี่ยนผ่าน
Redux เช่นเดียวกับ Flux ทำให้เราคิดในแง่ของการเปลี่ยนภาพ แบบจำลองทางจิตใจของการพัฒนาด้วย Redux นั้นค่อนข้างจะขับเคลื่อนโดยการกระทำและวิธีที่การกระทำเหล่านี้เปลี่ยนสถานะในตัวลดของเรา นั่นไม่เลว แต่ฉันพบว่าการคิดในแง่ของสถานะนั้นเหมาะสมกว่า — สถานะของแอพอาจอยู่ในและวิธีที่สถานะเหล่านี้แสดงถึงข้อกำหนดทางธุรกิจ
บทสรุป
แนวคิดของเครื่องจักรของรัฐในการเขียนโปรแกรม โดยเฉพาะอย่างยิ่งในการพัฒนา UI เป็นสิ่งที่เปิดหูเปิดตาสำหรับฉัน ฉันเริ่มเห็นเครื่องจักรของรัฐทุกหนทุกแห่งและฉันมีความปรารถนาที่จะเปลี่ยนไปใช้กระบวนทัศน์นั้นเสมอ ฉันเห็น ประโยชน์ของการมีสถานะและช่วงการเปลี่ยนภาพที่ชัดเจนยิ่งขึ้น ระหว่างกัน ฉันมักจะค้นหาวิธีทำให้แอปของฉันเรียบง่ายและอ่านง่าย ฉันเชื่อว่าเครื่องจักรของรัฐเป็นขั้นตอนในทิศทางนี้ แนวคิดนี้เรียบง่ายและทรงพลังในขณะเดียวกัน มีศักยภาพในการกำจัดแมลงจำนวนมาก