Topic 29 การรับมือกับโลกแห่งความเป็นจริง

สิ่งต่าง ๆ ไม่ได้เกิดขึ้นเอง แต่มีคนทำให้มันเกิดขึ้น

John F. Kennedy

ในสมัยก่อน ตอนที่ผู้เขียนหนังสือเล่มนี้ยังหนุ่มหล่อกันอยู่ คอมพิวเตอร์ยังไม่ค่อยยืดหยุ่นเท่าไหร่ เรามักจะจัดวิธีที่เราทำงานร่วมกับมันตามข้อจำกัดของมันเองครับ

ทุกวันนี้ เราคาดหวังมากกว่านั้น คอมพิวเตอร์ต้องปรับตัวเข้ากับโลกของ เรา ไม่ใช่ในทางกลับกัน และโลกของเราก็วุ่นวาย สิ่งต่าง ๆ เกิดขึ้นตลอดเวลา ของถูกย้ายไปมา เราเปลี่ยนใจกันอยู่เรื่อย ๆ …. และแอปพลิเคชันที่เราเขียนก็ต้องหาทางรับมือกับสิ่งเหล่านี้ให้ได้ครับ

หัวข้อนี้ทั้งหมดจะพูดถึงการเขียนแอปพลิเคชันที่ตอบสนองไวแบบนี้ครับ

เราจะเริ่มต้นด้วยแนวคิดของ event ครับ

Events (อีเวนต์)

An event คือสิ่งที่แสดงถึงการที่มีข้อมูลพร้อมใช้งานแล้ว มันอาจมาจากโลกภายนอก เช่น ผู้ใช้คลิกปุ่ม หรือราคาหุ้นมีการอัปเดต หรืออาจเป็นเรื่องภายใน เช่น ผลลัพธ์ของการคำนวณพร้อมแล้ว การค้นหาเสร็จสิ้น หรือแม้แต่เรื่องเล็ก ๆ น้อย ๆ อย่างการดึง element ตัวถัดไปในลิสต์ก็นับเป็น event ได้เช่นกัน

ไม่ว่าที่มาของ event จะเป็นอะไร ถ้าเราเขียนแอปพลิเคชันที่ตอบสนองต่อ event และปรับพฤติกรรมตาม event เหล่านั้น แอปพลิเคชันเหล่านั้นก็จะทำงานได้ดีขึ้นในโลกแห่งความเป็นจริง ผู้ใช้จะรู้สึกว่ามันโต้ตอบได้ดีขึ้น และตัวแอปพลิเคชันเองก็จะใช้ทรัพยากรได้อย่างคุ้มค่ามากขึ้นครับ

แต่เราจะเขียนแอปพลิเคชันแบบนี้ได้อย่างไร ถ้าไม่มีกลยุทธ์อะไรเลย เราก็จะพากันงงอย่างรวดเร็ว และแอปพลิเคชันของเราก็จะกลายเป็นโค้ดที่ผูกติดกันยุ่งเหยิงไปหมดครับ

มาดูกลยุทธ์ทั้งสี่ที่ช่วยเราได้กันครับ

  1. Finite State Machines (ระบบสถานะจำกัด)
  2. The Observer Pattern (แพทเทิร์นผู้สังเกตการณ์)
  3. Publish/Subscribe (รูปแบบเผยแพร่/สมัครรับข้อมูล)
  4. Reactive Programming and Streams (การเขียนโปรแกรมเชิงรีแอกทีฟและสตรีม)

Finite State Machines (ระบบสถานะจำกัด)

Dave พบว่าเขาเขียนโค้ดโดยใช้ Finite State Machine (FSM) แทบทุกสัปดาห์ บ่อยครั้งการ implement FSM จะใช้โค้ดแค่ไม่กี่บรรทัด แต่บรรทัดเหล่านั้นก็ช่วยคลี่คลายความยุ่งเหยิงที่อาจเกิดขึ้นได้มากทีเดียวครับ

การใช้ FSM นั้นง่ายมาก แต่นักพัฒนาหลายคนกลับหลีกเลี่ยงมัน ดูเหมือนจะมีความเชื่อว่ามันยาก หรือใช้ได้แค่กับงานที่เกี่ยวข้องกับฮาร์ดแวร์ หรือต้องใช้ไลบรารีที่เข้าใจยาก ซึ่งไม่มีข้อไหนเป็นความจริงเลยครับ

The Anatomy of a Pragmatic FSM (กายวิภาคของ FSM แบบ Pragmatic)

state machine โดยพื้นฐานแล้วก็คือข้อกำหนดว่าจะจัดการกับ event อย่างไร มันประกอบด้วยชุดของ state ต่าง ๆ โดยมีหนึ่งใน state เหล่านั้นเป็น current state สำหรับแต่ละ state เราจะระบุ event ที่มีความสำคัญกับ state นั้น และสำหรับแต่ละ event นั้น เราจะกำหนด current state ใหม่ของระบบ

ตัวอย่างเช่น เราอาจกำลังรับข้อความแบบ multipart จาก websocket โดยข้อความแรกคือ header ตามด้วยข้อความข้อมูล (data) จำนวนเท่าไหร่ก็ได้ และตามด้วยข้อความปิดท้าย (trailer) ซึ่งสามารถแสดงเป็น FSM ได้ดังนี้ครับ

An illustration explains a finite state machine diagram. The first state is the initial state. From the initial state, header message is passed to the reading message. The data message is imported in the reading message. From the initial state and the reading message state, the transition is done to error state and is detected and finally done. Also from the reading message state, trailer messages are accepted and finally done.

เราเริ่มต้นที่ “Initial state” ถ้าเราได้รับข้อความ header เราจะ transition ไปยัง state “Reading message” ถ้าเราได้รับอะไรก็ตามที่ไม่ใช่แบบนั้นขณะที่อยู่ใน initial state (เส้นที่มีเครื่องหมายดอกจัน) เราจะเปลี่ยนไปยัง state “Error” และจบการทำงาน

ขณะที่เราอยู่ใน state “Reading message” เราสามารถรับข้อความข้อมูลได้ ซึ่งจะทำให้เรายังคงอ่านต่อใน state เดิม หรือเราอาจรับข้อความปิดท้าย ซึ่งจะเปลี่ยนเราไปยัง state “Done” ส่วนอย่างอื่นนอกเหนือจากนี้จะทำให้เปลี่ยนไปยัง state error

สิ่งที่ดีเกี่ยวกับ FSM ก็คือเราสามารถแสดงมันได้ล้วน ๆ ในรูปแบบข้อมูล นี่คือตารางที่แสดง message parser ของเราครับ

A table shows message parser information.

ตารางแสดงข้อมูลเกี่ยวกับ message parser หัวคอลัมน์คือ header, data, trailer และ others ซึ่งแทน event ส่วนหัวแถวคือ initial และ reading ซึ่งแทน state แถวที่ 1 อ่านได้ว่า reading, error, error และ error แถวที่ 2 อ่านได้ว่า error, reading, done และ error

แถวในตารางแทน state ต่าง ๆ หากต้องการทราบว่าเมื่อ event เกิดขึ้นต้องทำอะไร ให้มองหาแถวของ current state แล้วไล่ไปตามคอลัมน์ที่แทน event นั้น เนื้อหาในเซลล์นั้นก็คือ state ใหม่นั่นเองครับ

โค้ดที่จัดการเรื่องนี้ก็เรียบง่ายไม่แพ้กันครับ

event/simple_fsm.rb

​ 1: TRANSITIONS = {
​ - ​ initial: ​{​ header: :reading ​},
​ - ​ reading: ​{​ data: :reading ​, ​ trailer: :done ​},
​ - }
​ 5:
​ - state = ​ :initial ​
​ -
​ - ​ while ​ state != ​ :done ​ && state != ​ :error ​
​ - msg = get_next_message()
​ 10: state = TRANSITIONS[state][msg.msg_type] || ​ :error ​
​ - ​ end ​

โค้ดที่ implement การ transition ระหว่าง state อยู่ที่บรรทัดที่ 10 มันจะ index ตาราง transition โดยใช้ current state แล้วจึง index transition สำหรับ state นั้นโดยใช้ประเภทของข้อความ ถ้าไม่มี state ใหม่ที่ตรงกัน มันจะตั้งค่า state เป็น :error ครับ

Adding Actions (การเพิ่ม Action)

FSM แบบล้วน ๆ อย่างที่เราเพิ่งดูไปนั้น เป็นเพียง event stream parser เอาต์พุตเดียวของมันคือ state สุดท้าย เราสามารถเสริมความสามารถของมันได้ด้วยการเพิ่ม action ที่ถูกกระตุ้นเมื่อมีการ transition บางอย่างเกิดขึ้น

ตัวอย่างเช่น เราอาจต้องดึงข้อความทุกสตริงในไฟล์ซอร์สโค้ดออกมา สตริงคือข้อความระหว่างเครื่องหมายคำพูด แต่ backslash ในสตริงจะ escape ตัวอักษรถัดไป ดังนั้น "Ignore \"quotes\"" จึงเป็นสตริงเดียว นี่คือ FSM ที่ทำสิ่งนี้ครับ

A state diagram of three states is shown.

แผนภาพ state แสดง state ทั้งสาม state แรกคือ look for string มี self loop สำหรับ process ดอกจัน state ที่สองคือ "in string" การ transition จาก state แรกไปยัง state ที่สองทำผ่าน "ch equals asterisk, do init result" มี self loop สำหรับ process "ch: anything else, do: add to result" state ที่สามคือ "copy next char" การ transition จาก state ที่สองไปยัง state ที่สามทำผ่าน "ch equals backslash, do add to result" การ transition จาก state ที่สามไปยัง state ที่สองทำผ่าน "ch equals anything, do add to result" การ transition จาก state ที่สองไปยัง state แรกทำผ่าน "ch equals double quote, do output result"

ครั้งนี้ แต่ละ transition จะมีป้ายกำกับสองอัน อันบนคือ event ที่กระตุ้นให้เกิด transition และอันล่างคือ action ที่ต้องทำระหว่างที่เปลี่ยนจาก state หนึ่งไปอีก state หนึ่ง

เราจะแสดงสิ่งนี้ในรูปแบบตาราง เหมือนที่เราทำไปครั้งก่อน แต่ในกรณีนี้ แต่ละรายการในตารางจะเป็นลิสต์สององค์ประกอบที่มี state ถัดไปและชื่อของ action ครับ

event/strings_fsm.rb

​ TRANSITIONS = {
​
​ ​ # current new state action to take ​
​ ​ #--------------------------------------------------------- ​
​
​ ​ look_for_string: ​{
​ ​ '"' ​ => [ ​ :in_string ​, ​ :start_new_string ​ ],
​ ​ :default ​ => [ ​ :look_for_string ​, ​ :ignore ​ ],
​ },
​
​ ​ in_string: ​{
​ ​ '"' ​ => [ ​ :look_for_string ​, ​ :finish_current_string ​ ],
​ ​ '\' ​ => [ ​ :copy_next_char ​, ​ :add_current_to_string ​ ],
​ ​ :default ​ => [ ​ :in_string ​, ​ :add_current_to_string ​ ],
​ },
​
​ ​ copy_next_char: ​{
​ ​ :default ​ => [ ​ :in_string ​, ​ :add_current_to_string ​ ],
​ },
​ }

เรายังเพิ่มความสามารถในการระบุ default transition ซึ่งจะถูกใช้ถ้า event ไม่ตรงกับ transition อื่นใดของ state นี้เลย

ทีนี้มาดูโค้ดกันครับ

event/strings_fsm.rb

​ state = ​ :look_for_string ​
​ result = []
​
​ ​ while ​ ch = STDIN.getc
​ state, action = TRANSITIONS[state][ch] || TRANSITIONS[state][​ :default ​]
​ ​ case ​ action
​ ​ when ​ ​ :ignore ​
​ ​ when ​ ​ :start_new_string ​
​ result = []
​ ​ when ​ ​ :add_current_to_string ​
​ result << ch
​ ​ when ​ ​ :finish_current_string ​
​ puts result.join
​ ​ end ​
​ ​ end ​

ตัวอย่างนี้คล้ายกับตัวอย่างก่อนหน้า ตรงที่เรา loop ผ่าน event ต่าง ๆ (ตัวอักษรใน input) เพื่อกระตุ้นให้เกิด transition แต่มันทำมากกว่าโค้ดก่อนหน้านี้ ผลลัพธ์ของแต่ละ transition คือทั้ง state ใหม่และชื่อของ action เราใช้ชื่อ action เพื่อเลือกโค้ดที่จะรันก่อนที่จะวนกลับไปที่ loop อีกครั้งครับ

โค้ดนี้เรียบง่ายมาก แต่ก็ทำงานได้จริง ยังมีตัวแปรอื่น ๆ อีกมากมาย เช่น ตาราง transition อาจใช้ anonymous function หรือ function pointer สำหรับ action หรือคุณอาจห่อโค้ดที่ implement state machine ไว้ในคลาสแยกต่างหาก พร้อม state ของตัวเอง และอื่น ๆ อีกมากมายครับ

ไม่มีกฎตายตัวว่าคุณต้องประมวลผล state transition ทั้งหมดพร้อมกัน ถ้าคุณกำลังทำตามขั้นตอนเพื่อสมัครสมาชิกผู้ใช้บนแอปของคุณ ก็มีแนวโน้มว่าจะมี transition หลายครั้งขณะที่พวกเขากรอกข้อมูล ยืนยันอีเมล ยอมรับคำเตือนตามกฎหมาย 107 ข้อที่แอปออนไลน์ต้องแสดงในปัจจุบัน และอื่น ๆ การเก็บ state ไว้ใน external storage แล้วใช้มันขับเคลื่อน state machine เป็นวิธีที่ดีเยี่ยมในการจัดการความต้องการของ workflow แบบนี้ครับ

State Machines Are a Start (State Machine เป็นแค่จุดเริ่มต้น)

นักพัฒนาใช้ state machine กันน้อยเกินไป และเราอยากสนับสนุนให้คุณมองหาโอกาสที่จะนำมันไปใช้ แต่มันก็ไม่ได้แก้ปัญหาทุกอย่างที่เกี่ยวข้องกับ event ได้ทั้งหมด ดังนั้นมาดูวิธีอื่น ๆ ในการมองปัญหาของการรับมือกับ event กันต่อครับ

The Observer Pattern (แพทเทิร์นผู้สังเกตการณ์)

ใน observer pattern เรามีแหล่งที่มาของ event เรียกว่า observable และมีลิสต์ของ client ที่เรียกว่า observers ซึ่งสนใจใน event เหล่านั้น

observer จะลงทะเบียนความสนใจของตัวเองกับ observable โดยทั่วไปแล้วจะทำผ่านการส่ง reference ของ function ที่จะถูกเรียก จากนั้นเมื่อ event เกิดขึ้น observable จะวนลูปไปตามลิสต์ observer ของมัน แล้วเรียก function ที่แต่ละตัวส่งมาให้ โดย event จะถูกส่งเป็น parameter ให้กับการเรียกนั้นครับ

นี่คือตัวอย่างง่าย ๆ ใน Ruby โมดูล Terminator ใช้เพื่อปิดแอปพลิเคชัน แต่ก่อนที่มันจะทำเช่นนั้น มันจะแจ้ง observer ทั้งหมดของมันว่าแอปพลิเคชันกำลังจะจบการทำงาน [39] พวกมันอาจใช้การแจ้งเตือนนี้เพื่อจัดการทรัพยากรชั่วคราว commit ข้อมูล และอื่น ๆ ครับ

event/observer.rb

​ ​ module ​ Terminator
​ CALLBACKS = []
​
​ ​ def ​ self.register(callback)
​ CALLBACKS << callback
​ ​ end ​
​
​ ​ def ​ self.exit(exit_status)
​ CALLBACKS.each { |callback| callback.(exit_status) }
​ exit!(exit_status)
​ ​ end ​
​ ​ end ​
​
​ Terminator.register(-> (status) { puts ​ "callback 1 sees ​​ #{ ​status​ } ​​ " ​ })
​ Terminator.register(-> (status) { puts ​ "callback 2 sees ​​ #{ ​status​ } ​​ " ​ })
​
​ Terminator.exit(99)
​ ​ $ ​​ ruby ​​ ​​ event/observer.rb ​
​ callback 1 sees 99
​ callback 2 sees 99

การสร้าง observable นั้นใช้โค้ดไม่มากเลย คุณแค่ push function reference เข้าไปในลิสต์ แล้วเรียก function เหล่านั้นเมื่อ event เกิดขึ้น นี่เป็นตัวอย่างที่ดีของกรณีที่ ไม่ควร ใช้ library ครับ

รูปแบบ observer/observable ถูกใช้มานานหลายทศวรรษแล้ว และมันก็รับใช้เราได้ดีมาตลอด มันพบได้บ่อยเป็นพิเศษในระบบ user interface ที่ callback ถูกใช้เพื่อแจ้งให้แอปพลิเคชันทราบว่ามีการโต้ตอบบางอย่างเกิดขึ้นครับ

แต่ observer pattern ก็มีปัญหา เพราะ observer แต่ละตัวต้องลงทะเบียนกับ observable มันจึงทำให้เกิด coupling นอกจากนี้ เนื่องจาก implementation ทั่วไปจะจัดการ callback แบบ inline โดย observable อย่าง synchronous มันจึงอาจทำให้เกิด bottleneck ด้านประสิทธิภาพได้ด้วยครับ

ปัญหานี้ถูกแก้ไขด้วยกลยุทธ์ถัดไป นั่นคือ Publish/Subscribe ครับ

Publish/Subscribe (รูปแบบเผยแพร่/สมัครรับข้อมูล)

Publish/Subscribe (pubsub) เป็นการทำให้ observer pattern กว้างขึ้น ในขณะเดียวกันก็แก้ปัญหาเรื่อง coupling และประสิทธิภาพไปด้วยครับ

ใน pubsub model เรามี publisher และ subscriber ซึ่งเชื่อมต่อกันผ่าน channel ต่าง ๆ channel เหล่านี้ถูก implement อยู่ในโค้ดอีกชุดหนึ่งแยกต่างหาก บางครั้งก็เป็น library บางครั้งก็เป็น process และบางครั้งก็เป็นโครงสร้างพื้นฐานแบบ distributed รายละเอียดการ implement ทั้งหมดนี้ถูกซ่อนไว้จากโค้ดของคุณครับ

ทุก channel มีชื่อของตัวเอง subscriber จะลงทะเบียนความสนใจใน channel ที่มีชื่อเหล่านี้หนึ่งช่องหรือมากกว่า และ publisher จะเขียน event ลงไปใน channel เหล่านั้น ต่างจาก observer pattern ตรงที่การสื่อสารระหว่าง publisher กับ subscriber ถูกจัดการอยู่นอกโค้ดของคุณ และอาจเป็นแบบ asynchronous ได้ครับ

แม้คุณจะสามารถ implement ระบบ pubsub พื้นฐานมาก ๆ ได้เอง แต่คุณก็คงไม่อยากทำแบบนั้น ผู้ให้บริการ cloud ส่วนใหญ่มีบริการ pubsub ให้ใช้ ซึ่งช่วยให้คุณเชื่อมต่อแอปพลิเคชันต่าง ๆ ทั่วโลกได้ ทุกภาษาที่ได้รับความนิยมจะมี pubsub library อย่างน้อยหนึ่งตัวครับ

Pubsub เป็นเทคโนโลยีที่ดีสำหรับการลด coupling ในการจัดการ event แบบ asynchronous มันช่วยให้เพิ่มหรือแทนที่โค้ดได้ อาจจะขณะที่แอปพลิเคชันกำลังทำงานอยู่ด้วยซ้ำ โดยไม่ต้องแก้โค้ดเดิม ข้อเสียคือมันอาจยากที่จะเห็นว่าเกิดอะไรขึ้นในระบบที่ใช้ pubsub อย่างหนัก คุณไม่สามารถมองที่ publisher แล้วรู้ทันทีว่า subscriber ตัวไหนเกี่ยวข้องกับข้อความใดข้อความหนึ่งครับ

เมื่อเทียบกับ observer pattern แล้ว pubsub เป็นตัวอย่างที่ดีของการลด coupling ด้วยการ abstract ขึ้นไปผ่าน interface ที่ใช้ร่วมกัน (channel) อย่างไรก็ตาม โดยพื้นฐานแล้วมันก็ยังคงเป็นแค่ระบบส่งข้อความ การสร้างระบบที่ตอบสนองต่อการรวมกันของ event หลาย ๆ ตัวจะต้องการมากกว่านี้ ดังนั้นมาดูวิธีที่เราจะเพิ่มมิติของเวลาเข้าไปในการประมวลผล event กันครับ

Reactive Programming, Streams, and Events (การเขียนโปรแกรมเชิงรีแอกทีฟ สตรีม และอีเวนต์)

ถ้าคุณเคยใช้สเปรดชีตมาก่อน คุณก็คงคุ้นเคยกับ reactive programming แล้ว ถ้าเซลล์หนึ่งมีสูตรที่อ้างอิงถึงเซลล์ที่สอง การอัปเดตเซลล์ที่สองนั้นก็จะทำให้เซลล์แรกอัปเดตตามไปด้วย ค่าต่าง ๆ จะ react ตามการเปลี่ยนแปลงของค่าที่มันใช้ครับ

มี framework มากมายที่ช่วยเรื่อง reactivity ระดับข้อมูลแบบนี้ ในโลกของ browser React และ Vue.js เป็นตัวเลือกยอดนิยมในปัจจุบัน (แต่เนื่องจากนี่คือ JavaScript ข้อมูลนี้ก็คงล้าสมัยไปแล้วก่อนที่หนังสือเล่มนี้จะพิมพ์เสร็จด้วยซ้ำ)

เห็นได้ชัดว่า event ก็สามารถใช้เพื่อกระตุ้น reaction ในโค้ดได้เช่นกัน แต่มันก็ไม่ใช่เรื่องง่ายเสมอไปที่จะเชื่อมมันเข้าด้วยกัน นั่นคือจุดที่ stream เข้ามามีบทบาทครับ

stream ช่วยให้เราปฏิบัติต่อ event ราวกับว่ามันเป็น collection ของข้อมูล ราวกับว่าเรามีลิสต์ของ event ที่ยาวขึ้นเมื่อมี event ใหม่มาถึง ความสวยงามของมันก็คือเราสามารถปฏิบัติต่อ stream เหมือนกับ collection อื่น ๆ ได้เลย เราสามารถจัดการ รวม กรอง และทำสิ่งต่าง ๆ เกี่ยวกับข้อมูลอื่น ๆ ที่เรารู้จักกันดี เราสามารถรวม event stream กับ collection ปกติเข้าด้วยกันได้ด้วยซ้ำ และ stream ก็สามารถเป็นแบบ asynchronous ได้ ซึ่งหมายความว่าโค้ดของคุณจะมีโอกาสตอบสนองต่อ event ในขณะที่มันมาถึงครับ

baseline de facto ในปัจจุบันสำหรับการจัดการ event แบบ reactive ถูกกำหนดไว้บนเว็บไซต์ http://reactivex.io ซึ่งกำหนดชุดหลักการที่ไม่ผูกติดกับภาษาใดภาษาหนึ่ง และมีเอกสารเกี่ยวกับ implementation ที่พบได้ทั่วไปบางส่วน ในที่นี้เราจะใช้ library RxJs สำหรับ JavaScript ครับ

ตัวอย่างแรกของเราจะเอา stream สองตัวมา zip เข้าด้วยกัน ผลลัพธ์คือ stream ใหม่ที่แต่ละ element มีหนึ่งรายการจาก input stream แรก และหนึ่งรายการจาก stream อีกตัว ในกรณีนี้ stream แรกคือลิสต์ของชื่อสัตว์ห้าชื่อธรรมดา ๆ ส่วน stream ที่สองน่าสนใจกว่า มันคือ interval timer ที่สร้าง event ทุก 500ms เนื่องจาก stream ทั้งสองถูก zip เข้าด้วยกัน ผลลัพธ์จะถูกสร้างขึ้นก็ต่อเมื่อมีข้อมูลพร้อมในทั้งสองฝั่ง ดังนั้น result stream ของเราจึงปล่อยค่าออกมาแค่ทุกครึ่งวินาทีครับ

event/rx0/index.js

​ ​ import ​ * ​ as ​ Observable ​ from ​ ​ 'rxjs' ​
​ ​ import ​ { logValues } ​ from ​ ​ "../rxcommon/logger.js" ​
​
​ ​ let ​ animals = Observable.​ of ​(​ "ant" ​, ​ "bee" ​, ​ "cat" ​, ​ "dog" ​, ​ "elk" ​)
​ ​ let ​ ticker = Observable.interval(500)
​
​ ​ let ​ combined = Observable.zip(animals, ticker)
​
​ combined.subscribe(next => logValues(JSON.stringify(next)))

โค้ดนี้ใช้ logging function ง่าย ๆ [40] ซึ่งเพิ่มรายการเข้าไปในลิสต์บนหน้าต่าง browser แต่ละรายการจะถูกประทับเวลาด้วยจำนวนมิลลิวินาทีนับตั้งแต่โปรแกรมเริ่มทำงาน นี่คือสิ่งที่มันแสดงสำหรับโค้ดของเราครับ

An output is shown different timestamps. The output at 502 milliseconds is ["ant", 0]; The output at 1002 milliseconds is ["bee", 1]; The output at 1502 milliseconds is ["cat", 2]; The output at 2002 milliseconds is ["dog", 3]; The output at 2502 milliseconds is ["elk", 4];

สังเกตที่ timestamp เราได้รับ event หนึ่งจาก stream ทุก 500ms แต่ละ event มี serial number (สร้างโดย interval observable) และชื่อของสัตว์ตัวถัดไปจากลิสต์ เมื่อดูแบบ live ใน browser บรรทัด log จะปรากฏขึ้นทุกครึ่งวินาทีครับ

event stream โดยทั่วไปจะถูกเติมข้อมูลตามที่ event เกิดขึ้น ซึ่งหมายความว่า observable ที่เติมข้อมูลให้มันสามารถรันแบบขนานกันได้ นี่คือตัวอย่างที่ดึงข้อมูลเกี่ยวกับผู้ใช้จากไซต์ระยะไกล สำหรับตัวอย่างนี้เราจะใช้ https://reqres.in ซึ่งเป็นไซต์สาธารณะที่มี REST interface แบบเปิดให้ใช้งาน ในฐานะส่วนหนึ่งของ API เราสามารถดึงข้อมูลของผู้ใช้ (ปลอม) คนใดคนหนึ่งได้โดยการทำ GET request ไปที่ users/«id» โค้ดของเราจะดึงข้อมูลผู้ใช้ที่มี ID 3, 2 และ 1 ครับ

event/rx1/index.js

​ ​ import ​ * ​ as ​ Observable ​ from ​ ​ 'rxjs' ​
​ ​ import ​ { mergeMap } ​ from ​ ​ 'rxjs/operators' ​
​ ​ import ​ { ajax } ​ from ​ ​ 'rxjs/ajax' ​
​ ​ import ​ { logValues } ​ from ​ ​ "../rxcommon/logger.js" ​
​
​ ​ let ​ users = Observable.​ of ​(3, 2, 1)
​
​ ​ let ​ result = users.pipe(
​ mergeMap((user) => ajax.getJSON(​ https://reqres.in/api/users/ ​${user}​ ​))
​ )
​
​ result.subscribe(
​ resp => logValues(JSON.stringify(resp.data)),
​ err => console.error(JSON.stringify(err))
​ )

รายละเอียดภายในของโค้ดไม่สำคัญเท่าไหร่นัก สิ่งที่น่าตื่นเต้นคือผลลัพธ์ ดังที่แสดงในภาพหน้าจอต่อไปนี้ครับ

An output is shown different timestamps. Information about id: 2 is displayed at 82 milliseconds, information about id: 1 is displayed at 132 milliseconds, and information about id: 3 is displayed at 133 milliseconds.

ลองดูที่ timestamp สิ ทั้งสาม request หรือ stream แยกกันสามตัว ถูกประมวลผลแบบขนานกัน ตัวแรกที่กลับมา สำหรับ id 2 ใช้เวลา 82ms และอีกสองตัวถัดมากลับมาหลังจากนั้น 50 และ 51ms ตามลำดับครับ

Streams of Events Are Asynchronous Collections (สตรีมของอีเวนต์คือ Collection แบบ Asynchronous)

ในตัวอย่างก่อนหน้านี้ ลิสต์ของ user ID ของเรา (ใน observable users ) เป็นแบบ static แต่มันไม่จำเป็นต้องเป็นแบบนั้นเสมอไป บางทีเราอาจต้องการรวบรวมข้อมูลนี้เมื่อผู้คน login เข้ามาที่ไซต์ของเรา สิ่งที่เราต้องทำก็แค่สร้าง observable event ที่มี user ID ของพวกเขาเมื่อ session ของพวกเขาถูกสร้างขึ้น แล้วใช้ observable นั้นแทนตัวที่เป็น static จากนั้นเราก็จะดึงรายละเอียดเกี่ยวกับผู้ใช้ขณะที่เราได้รับ ID เหล่านี้ และคงจะเก็บมันไว้ที่ไหนสักแห่งครับ

นี่คือ abstraction ที่ทรงพลังมาก เราไม่จำเป็นต้องคิดถึงเวลาว่าเป็นสิ่งที่เราต้องจัดการอีกต่อไป event stream รวมการประมวลผลแบบ synchronous และ asynchronous เข้าด้วยกันภายใต้ API ที่ใช้งานสะดวกร่วมกันครับ

Events Are Ubiquitous (อีเวนต์อยู่ทุกหนทุกแห่ง)

event มีอยู่ทุกที่ บางอย่างก็ชัดเจน เช่น การคลิกปุ่ม ตัวจับเวลาที่หมดอายุ บางอย่างก็ไม่ชัดเจนนัก เช่น มีคน login เข้ามา บรรทัดหนึ่งในไฟล์ที่ตรงกับ pattern แต่ไม่ว่าที่มาของมันจะเป็นอะไร โค้ดที่ถูกออกแบบมาโดยยึด event เป็นศูนย์กลางจะตอบสนองได้ดีขึ้นและลด coupling ได้ดีกว่าโค้ดแบบเชิงเส้นทั่วไปครับ

Related Sections Include (หัวข้อที่เกี่ยวข้อง)

  • หัวข้อ 28, ​ Decoupling (การลดการผูกติดกัน) ​
  • หัวข้อ 36, ​ Blackboards (แบล็กบอร์ด) ​

Exercises (แบบฝึกหัด)

Exercise 19 ( คำตอบที่เป็นไปได้ )

ในหัวข้อ FSM เราพูดถึงว่าคุณสามารถย้าย implementation ของ generic state machine ไปไว้ในคลาสของตัวเองได้ คลาสนั้นน่าจะถูก initialize โดยการส่งตาราง transition และ initial state เข้าไปครับ

ลองสร้าง string extractor ด้วยวิธีนั้นดูสิครับ

Exercise 20 ( คำตอบที่เป็นไปได้ )

เทคโนโลยีเหล่านี้ (อาจใช้ร่วมกัน) แบบไหนจะเหมาะกับสถานการณ์ต่อไปนี้ครับ

  • ถ้าคุณได้รับ event network interface down สามครั้งภายในห้านาที ให้แจ้งเตือนทีม operations

  • ถ้าเป็นช่วงหลังพระอาทิตย์ตก และมีการตรวจจับความเคลื่อนไหวที่บริเวณล่างของบันได ตามด้วยการตรวจจับความเคลื่อนไหวที่บริเวณบนของบันได ให้เปิดไฟชั้นบน

  • คุณต้องการแจ้งระบบรายงานต่าง ๆ ว่ามีคำสั่งซื้อเสร็จสมบูรณ์แล้ว

  • เพื่อพิจารณาว่าลูกค้ามีคุณสมบัติผ่านสำหรับสินเชื่อรถยนต์หรือไม่ แอปพลิเคชันจำเป็นต้องส่ง request ไปยัง backend service สามตัว แล้วรอผลลัพธ์กลับมา