"Event-Driven" หมายความว่าอะไรกันแน่?
บทความนี้เป็น บทแปลภาษาไทยอย่างไม่เป็นทางการ ของ “What do you mean by “Event-Driven”?” โดย Martin Fowler เผยแพร่ครั้งแรกบน martinfowler.com เมื่อ 7 กุมภาพันธ์ 2017
แปลและเรียบเรียงเพื่อการศึกษา — ไดอะแกรมประกอบและตัวอย่างเชิงเล่าเรื่อง (เช่น ระบบประกันภัย) เป็นสิ่งที่ผู้แปลสร้างขึ้นใหม่เพื่อช่วยความเข้าใจ ต้นฉบับไม่มีไดอะแกรมและไม่ได้ยกตัวอย่างเหล่านี้ ส่วนศัพท์เทคนิคคงไว้เป็นภาษาอังกฤษพร้อมคำอธิบายไทยเมื่อพบครั้งแรก ลิขสิทธิ์ของเนื้อหาต้นฉบับเป็นของผู้เขียน
ในช่วงไม่กี่ปีที่ผ่านมา เราได้เห็นความสนใจอย่างมากต่อแนวคิดการสร้างระบบรอบ ๆ “event” (เหตุการณ์) หลายคนสนับสนุนให้ใช้สถาปัตยกรรมแบบ event-driven กับระบบของตัวเอง โดยชูข้อดีเรื่องการลด coupling (ความผูกติดกันระหว่างส่วนต่าง ๆ) ความยืดหยุ่นในการปรับขยาย และความคล่องตัวในการต่อยอด
แต่เช่นเดียวกับคำฮิตอื่น ๆ พอคำว่า “event-driven” ถูกใช้กันแพร่หลาย มันก็เริ่มถูกใช้อย่างพร่ำเพรื่อจนความหมายพร่าเลือน
ไม่นานมานี้ผู้เขียนได้ชวนเพื่อนร่วมงานหลายคนที่ทำงานในด้านนี้มารวมตัวกันจัด workshop เพื่อแลกเปลี่ยนประสบการณ์กัน ตลอดการพูดคุยเรากลับพบปัญหาร่วมกันอย่างหนึ่ง นั่นคือผู้คนใช้คำว่า “event-driven” เพื่อสื่อถึงสิ่งที่แตกต่างกันอยู่หลายอย่าง
เพื่อให้เข้าใจตรงกัน เราจึงพยายามระบุ pattern จำนวนหนึ่งออกมาให้ชัด ซึ่งแต่ละอันสามารถหยิบมาใช้ร่วมกันหรือแยกกันก็ได้ สุดท้ายเรามุ่งความสนใจไปที่4 pattern
ปัญหาไม่ได้อยู่ที่ pattern เหล่านี้แต่ละอัน แต่ละอันล้วนมีประโยชน์ในบริบทของมันเอง ปัญหาอยู่ที่การ เหมารวม มันเข้าด้วยกันภายใต้ร่มคำเดียว เพราะเมื่อคนสองคนพูดคำว่า “event-driven” แต่ในหัวคิดถึง pattern คนละอัน บทสนทนาก็คลาดเคลื่อนกันตั้งแต่ต้น
การแยกแยะให้ชัดว่าเรากำลังพูดถึง pattern ตัวไหน จึงเป็นก้าวแรกที่ช่วยให้คิดและออกแบบได้ตรงจุด
- Event Notification (การแจ้งเตือนด้วยเหตุการณ์) — ระบบส่งสัญญาณออกไปบอกว่ามีบางอย่างเปลี่ยนไป โดยไม่คาดหวังคำตอบกลับ
- Event-Carried State Transfer (การส่งต่อสถานะไปพร้อมกับเหตุการณ์) — event พก “สถานะ” (state) ที่ผู้รับต้องใช้มาให้ครบ ผู้รับจึงไม่ต้องย้อนกลับไปถามแหล่งข้อมูลต้นทาง
- Event Sourcing (การใช้เหตุการณ์เป็นแหล่งความจริง) — เก็บทุกการเปลี่ยนแปลงไว้เป็นลำดับของ event แล้วสามารถ “เล่นซ้ำ” (replay) เพื่อสร้างสถานะปัจจุบันขึ้นใหม่ได้เสมอ
- CQRS (การแยกความรับผิดชอบระหว่างการสั่งงานกับการสอบถาม) — แยก model ที่ใช้ “เขียน/สั่งงาน” (command) ออกจาก model ที่ใช้ “อ่าน” (query)
Event Notification — การแจ้งเตือนด้วยเหตุการณ์
หัวข้อที่มีชื่อว่า “Event Notification — การแจ้งเตือนด้วยเหตุการณ์”pattern แรกและเป็นที่คุ้นเคยที่สุดคือ Event Notification (การแจ้งเตือนด้วยเหตุการณ์) แนวคิดคือ เมื่อมีบางอย่างเปลี่ยนแปลงภายใน domain ของระบบหนึ่ง ระบบนั้นจะส่ง event message (ข้อความเหตุการณ์) ออกไปเพื่อ แจ้งให้ระบบอื่นรู้ ว่ามีการเปลี่ยนแปลงเกิดขึ้น
หัวใจสำคัญคือ ระบบต้นทางที่ส่ง event ออกไปนั้น ไม่ได้คาดหวังคำตอบกลับ และมักไม่รู้ด้วยซ้ำว่าใครเป็นผู้รับ
ตัวอย่างระบบประกันภัย (Customer Management / Insurance Quoting) ที่ใช้เดินเรื่องในหัวข้อนี้และหัวข้อถัดไป รวมถึงไดอะแกรมประกอบทั้งหมด เป็นสิ่งที่ผู้แปลสร้างขึ้นเองเพื่อช่วยให้เห็นภาพ ไม่ปรากฏในบทความต้นฉบับ
ต้นฉบับของ Fowler ไม่ได้ยกตัวอย่างใดสำหรับ Event Notification และสำหรับ Event-Carried State Transfer ก็เพียงเอ่ยถึง “ระบบจัดการลูกค้าที่ยิง event ออกไปเมื่อลูกค้าแก้ไขข้อมูล เช่น ที่อยู่” อย่างกว้าง ๆ เท่านั้น
ลองนึกถึงตัวอย่างของบริษัทประกันภัย สมมติว่าลูกค้าคนหนึ่งย้ายที่อยู่ การเปลี่ยนที่อยู่นี้ถูกจัดการโดยระบบ Customer Management (ระบบจัดการข้อมูลลูกค้า) แต่การย้ายที่อยู่อาจส่งผลต่อเบี้ยประกันของลูกค้าคนนั้นด้วย ซึ่งเป็นเรื่องของระบบ Insurance Quoting (ระบบคำนวณเบี้ยประกัน)
ในแบบดั้งเดิม code ในระบบจัดการลูกค้าอาจเรียกไปยังระบบคำนวณเบี้ยประกันโดยตรง
แต่ด้วย event notification ระบบจัดการลูกค้าจะเพียงประกาศ event ออกไปว่า “Customer Address Changed” (ที่อยู่ลูกค้าเปลี่ยนแล้ว) ส่วนระบบคำนวณเบี้ยประกันที่สนใจ event ชนิดนี้ก็คอยฟังอยู่ แล้วตอบสนองด้วยตัวเอง
flowchart LR CM["Customer Management"] -->|"emits Customer Address Changed"| BUS["Event Bus"] BUS -->|"notify"| INS["Insurance Quoting"] BUS -->|"notify"| OTHER["Other Consumers"] INS -.->|"query back for details"| CM
ภาพ 1 — Event Notification: ระบบ Customer Management ประกาศ event ออกไปโดยไม่รู้ว่าใครฟัง ระบบที่สนใจ (เช่น Insurance Quoting) รับ event มาแล้วทำงานต่อของตัวเอง โดยทั่วไป event จะพกข้อมูลติดมาแค่พอบอกว่า “เกิดอะไรขึ้น” กับ id สำหรับอ้างอิงเท่านั้น ถ้าผู้รับต้องการรายละเอียดเพิ่มก็ย้อนกลับไป query ที่ต้นทาง (ลูกศรประ)
ประโยชน์ที่ชัดเจนของ pattern นี้คือระดับของ decoupling (การลดความผูกติดกันระหว่างส่วนต่าง ๆ) ที่มันมอบให้ ระบบต้นทางไม่จำเป็นต้องรู้อะไรเลยเกี่ยวกับผู้รับ เราสามารถเพิ่มผู้รับรายใหม่เข้ามาฟัง event เดิมได้โดยไม่ต้องแก้ code ของต้นทางแม้แต่บรรทัดเดียว
ความรับผิดชอบถูกแยกออกจากกันอย่างสวยงาม domain ของระบบต้นทางไม่ปะปนไปด้วยตรรกะของระบบปลายทาง
หากสนใจโครงสร้างเชิง code ของแนวคิดนี้ ดูเพิ่มได้ที่ Domain Events และ Event Aggregator
จุดที่ต้องระวังคือความต่างระหว่าง event กับ command (คำสั่ง) event คือการประกาศว่า มีบางอย่างเกิดขึ้นไปแล้ว เช่น “ที่อยู่ถูกเปลี่ยนแล้ว” มันเป็นการบอกข้อเท็จจริง ไม่ได้สั่งให้ผู้รับรายใดทำอะไรเป็นการเฉพาะ ส่วน command คือการร้องขอให้ผู้รับที่เจาะจงลงมือทำงานบางอย่าง
เส้นแบ่งที่ดีคือ ต้นทางแคร์ผลลัพธ์หรือไม่ ถ้าต้นทางไม่แคร์ว่าจะเกิดอะไรตามมา นั่นคือ event ที่แท้จริง แต่ถ้าต้นทางแอบคาดหวังว่าผู้รับต้องทำสิ่งใดสิ่งหนึ่ง มันก็คือ command ที่ปลอมตัวมาในคราบ event
นี่เป็นเรื่องละเอียดอ่อนที่พลาดกันได้ง่าย และเมื่อพลาดแล้วก็จะได้ระบบที่ผูกติดกันแน่นทั้งที่ตั้งใจจะให้หลวม
แต่ event notification ก็มี ข้อเสีย สำคัญอยู่ นั่นคือ มันไม่มีที่ไหนบอกภาพรวมของพฤติกรรมทั้งระบบไว้เลย เมื่อลองมองว่า “พอที่อยู่เปลี่ยน แล้วเกิดอะไรขึ้นบ้าง” คำตอบจะกระจัดกระจายอยู่ตาม event handler หลายตัวที่แยกกันคนละที่
ไม่มีจุดใดจุดหนึ่งใน code ที่เราเปิดดูแล้วเห็น flow (ลำดับการทำงาน) ทั้งหมดได้ ตรรกะของ flow กลายเป็นสิ่งที่ “เกิดขึ้นเอง” (emergent) จากการที่ handler แต่ละตัวตอบสนอง event ของมัน
ความสามารถในการเพิ่ม handler ได้อิสระ ซึ่งช่วยเราตอนสร้างระบบ กลับกลายเป็นอุปสรรคตอนต้องทำความเข้าใจและ debug ระบบ เพราะเราไม่อาจไล่ flow จากการอ่าน code ตรง ๆ ได้อีก
ทางแก้คือต้องลงทุนกับ monitoring และ logging ที่ดีพอจะประกอบร่าง flow ที่เกิดขึ้นจริงกลับขึ้นมาได้ นี่ไม่ได้แปลว่า event notification เป็นสิ่งไม่ดี แต่มันเรียกร้องวินัยและเครื่องมือสังเกตการณ์ที่เหมาะสมเพื่อชดเชยความชัดเจนของ flow ที่หายไป
Event-Carried State Transfer — การส่งต่อสถานะไปพร้อมกับเหตุการณ์
หัวข้อที่มีชื่อว่า “Event-Carried State Transfer — การส่งต่อสถานะไปพร้อมกับเหตุการณ์”pattern ที่สองต่อยอดจาก event notification โดยตรง สังเกตว่าในภาพแรก เวลาผู้รับต้องการข้อมูลเพิ่ม มันต้องย้อนกลับไป query ที่ระบบต้นทาง การย้อนถามแบบนี้สร้างภาระให้ต้นทางและผูกผู้รับให้ต้องพึ่งพาต้นทางในเวลาจริง
Event-Carried State Transfer (การส่งต่อสถานะไปพร้อมกับเหตุการณ์) แก้ปัญหานี้ด้วยการ ใส่ข้อมูลสถานะที่ผู้รับจำเป็นต้องใช้ลงไปใน event ให้ครบ ตั้งแต่ต้น
กลับไปที่ตัวอย่างเดิม แทนที่ event “Customer Address Changed” จะพกมาแค่ id ของลูกค้า มันจะพกข้อมูลลูกค้าที่เปลี่ยนแปลง (เช่น ที่อยู่ใหม่ทั้งชุด) ติดมาด้วย เมื่อระบบคำนวณเบี้ยประกันได้รับ event นี้ มันก็มีทุกอย่างที่ต้องใช้อยู่แล้ว ไม่ต้องย้อนกลับไปถามระบบจัดการลูกค้าอีก
โดยทั่วไปผู้รับจะเก็บ สำเนาข้อมูล (replica) ของสิ่งที่ตนสนใจไว้ในฝั่งตัวเอง แล้วคอยอัปเดตสำเนานั้นทุกครั้งที่มี event เข้ามา
flowchart LR
CM["Customer Management<br/>source of truth"] -->|"event carries full customer data"| INS
subgraph INS["Insurance Quoting"]
direction TB
LOGIC["Quoting Logic"]
COPY["Local Replica of Customer Data"]
LOGIC --> COPY
end
ภาพ 2 — Event-Carried State Transfer: event พาข้อมูลลูกค้าที่จำเป็นมาให้ครบ ระบบ Insurance Quoting จึงเก็บสำเนาข้อมูลลูกค้าไว้ในฝั่งตัวเอง และทำงานต่อได้โดยไม่ต้องย้อนถามต้นทาง สังเกตว่าไม่มีลูกศร query ย้อนกลับเหมือนภาพแรกอีกแล้ว
ข้อดีของแนวทางนี้มาจากการที่ผู้รับไม่ต้องพึ่งต้นทางในเวลาจริง อย่างแรกคือ ลดภาระ (load) ที่ตกกับระบบต้นทาง เพราะไม่มีการ query ย้อนกลับเข้ามารบกวนอีก
อย่างที่สองคือ เพิ่มความพร้อมใช้งาน (availability) และความทนทาน (resilience) ของผู้รับ เพราะแม้ระบบต้นทางจะล่มชั่วคราว ผู้รับก็ยังทำงานต่อได้ด้วยสำเนาข้อมูลที่ตนถืออยู่ กล่าวคือมันลด coupling ในเชิงเวลา (temporal coupling) ลงไปอีกขั้น
แต่ทุกอย่างมีราคาของมัน ข้อเสียคือตอนนี้เรามี ข้อมูลจำลองซ้ำ (replicated data) กระจายอยู่หลายที่ ทุกระบบที่รับ event ต่างเก็บสำเนาข้อมูลที่ตนสนใจไว้เอง นั่นหมายถึงพื้นที่จัดเก็บที่มากขึ้น และที่สำคัญกว่านั้นคือเราเข้าสู่โลกของ eventual consistency (ความสอดคล้องที่ตามมาทีหลัง)
สำเนาข้อมูลในระบบต่าง ๆ อาจล้าสมัยชั่วขณะจนกว่า event จะไหลไปถึงและอัปเดตให้ตรงกัน พูดง่าย ๆ คือเรากำลังรับข้อดีและข้อเสียแบบคลาสสิกของการทำ replication มาเต็ม ๆ นั่นเอง
Event Sourcing — การใช้เหตุการณ์เป็นแหล่งความจริง
หัวข้อที่มีชื่อว่า “Event Sourcing — การใช้เหตุการณ์เป็นแหล่งความจริง”pattern ที่สามชื่อ Event Sourcing (การใช้เหตุการณ์เป็นแหล่งความจริง) และมักเป็นตัวที่สร้างความสับสนมากที่สุด
แนวคิดหลักคือ เก็บทุกการเปลี่ยนแปลงที่เกิดกับสถานะของ application ไว้เป็นลำดับของ event ในบันทึกที่เรียกว่า event log แล้วเราจะสามารถ สร้างสถานะปัจจุบันขึ้นใหม่ได้เสมอด้วยการเล่น event ทั้งหมดซ้ำ (replay) ตั้งแต่ต้นจนจบ
หัวใจของ event sourcing คือการสลับบทบาทของ “แหล่งความจริง” event log ต่างหากคือ system of record (แหล่งบันทึกที่ถือว่าเชื่อถือได้จริง) ส่วนสถานะปัจจุบันที่เราใช้งานอยู่นั้นเป็นเพียง ผลลัพธ์ที่ derive (สืบทอด) ออกมา จาก event เท่านั้น เปรียบได้เหมือน cache ที่สร้างขึ้นเพื่อความสะดวก
ถ้าลบสถานะปัจจุบันทิ้งไปทั้งหมด เราก็สร้างมันกลับขึ้นมาใหม่จาก event log ได้เสมอ
ตัวอย่างที่ช่วยให้เห็นภาพชัดที่สุดคือ version control system อย่าง Git เพราะ repository ไม่ได้เก็บแค่ “สถานะล่าสุดของ file” แต่เก็บทุก commit (ทุกการเปลี่ยนแปลง) ไว้เป็นลำดับ ส่วน working copy ที่เราเห็นในเครื่องนั้นเป็นเพียงสิ่งที่ derive ออกมาจากการเล่น commit ทั้งหมดซ้ำ
อีกตัวอย่างคือ บัญชีแบบ ledger ของนักบัญชี ที่ไม่มีการลบหรือแก้รายการเก่า มีแต่การเพิ่มรายการใหม่เข้าไป ยอดคงเหลือเป็นผลรวมที่คำนวณจากรายการทั้งหมด
flowchart LR CMD["Command"] --> APP["Application Logic"] APP -->|"append event"| LOG["Event Log<br/>system of record"] LOG -->|"replay"| STATE["Current State<br/>derived"] LOG -.->|"periodically"| SNAP["Snapshot"] SNAP -.->|"speed up rebuild"| STATE
ภาพ 3 — Event Sourcing: คำสั่งเข้ามาที่ตรรกะของ app ซึ่งไม่ได้เขียนทับสถานะโดยตรง แต่ append (ต่อท้าย) event ลงใน event log ที่เป็นแหล่งความจริง สถานะปัจจุบันเป็นเพียงผล derive จากการ replay event ทั้งหมด ส่วน snapshot คือภาพนิ่งของสถานะ ณ ช่วงเวลาหนึ่งที่ทำไว้เป็นระยะ เพื่อไม่ต้อง replay ตั้งแต่ event แรกทุกครั้ง
event sourcing ไม่จำเป็นต้องเป็น asynchronous หลายคนเข้าใจว่ามันต้องมาคู่กับการสื่อสารแบบไม่ประสานเวลาเสมอ แต่ที่จริงไม่ใช่ ลองนึกถึงการ commit ลง git repository ในเครื่องตัวเอง หรือการ commit เข้า version control แบบรวมศูนย์อย่าง SVN นั่นล้วนเป็นการทำงานแบบ synchronous ทั้งสิ้น
ไม่ใช่ทุกส่วนของระบบที่ต้องรับรู้หรือแตะ event log อีกความเข้าใจผิดหนึ่งคือคิดว่าทุกคนที่ใช้ระบบ event-sourced จะต้องเข้าใจและเข้าถึง event log เพื่อดึงข้อมูล
ความจริงคือส่วนใหญ่ของระบบทำงานอยู่บน “สำเนาใช้งาน” (working copy) ที่ derive ออกมาได้ก็พอ มีเพียงองค์ประกอบที่จำเป็นต้องใช้ข้อมูลจาก event log จริง ๆ เท่านั้นที่ควรเข้าไปยุ่งกับมันโดยตรง
เปรียบเหมือนตอนนี้ผู้เขียนพิมพ์ต้นฉบับอยู่ในโปรแกรมแก้ไขข้อความที่ไม่รู้อะไรเลยเกี่ยวกับ commit ทั้งหลายใน source tree มันเพียงมองเห็นว่ามี file อยู่บน disk เท่านั้น
เรื่องวิธีจัดเก็บภายในไม่เกี่ยวกับการเป็น event sourcing บางคนแย้งว่า git ไม่ใช่ event sourcing เพราะภายใน .git/objects มันเก็บเป็น snapshot ของ file และ tree (และใช้ packfile ที่ผสม snapshot กับ delta เพื่อประสิทธิภาพ)
แต่การที่ระบบจะเก็บข้อมูลภายในเป็น “การเปลี่ยนแปลง” หรือ “ภาพนิ่ง” นั้นไม่เกี่ยวกับว่ามันเป็น event-sourced หรือไม่
ตราบใดที่ git ยังเรียกดูรายการการเปลี่ยนแปลงทั้งหมดออกมาให้เราได้ตามต้องการ มันก็ยังเป็น event sourcing อยู่ดี ไม่ว่าเบื้องหลังจะจัดเก็บด้วยกลยุทธ์ใดก็ตาม
ประโยชน์ ของ event sourcing นั้นทรงพลังมาก เพราะเมื่อเราถือประวัติทุกก้าวไว้ครบ:
- การตรวจสอบย้อนหลังที่สมบูรณ์ (Audit) — เรามีบันทึกทุกสิ่งที่เคยเกิดขึ้นในระบบ ซึ่งมีค่ามหาศาลสำหรับงานที่ต้องตรวจสอบย้อนหลังหรืออยู่ภายใต้ข้อกำหนดด้านกฎระเบียบ (บัญชีเดินสะพัดก็คือ event source ของยอดคงเหลือในบัญชีนั่นเอง)
- สถานะย้อนเวลา (temporal / historic state) — เราสร้างสถานะ ณ จุดเวลาใดในอดีตขึ้นมาใหม่ได้ ตอบคำถามอย่าง “เมื่อวันอังคารที่แล้วสถานะเป็นอย่างไร” ได้จริง และเข้าใจว่า มาถึงสถานะปัจจุบันได้อย่างไร
- สำรวจทางเลือก (what-if / alternative history) — เราสามารถ replay event ชุดเดิมโดยแทรก event สมมติเข้าไป เพื่อทดลองดูว่า “ถ้าเหตุการณ์เป็นอีกแบบจะได้ผลอย่างไร”
- Memory Image (การเก็บสถานะไว้ในหน่วยความจำ) — เพราะสถานะสร้างใหม่จาก event ได้เสมอ เราจึงอาจเลือกเก็บสำเนาใช้งานไว้ในหน่วยความจำล้วน ๆ แบบไม่ต้องคงทน (non-durable) โดยไม่ต้องพึ่งฐานข้อมูลในแบบเดิม ๆ
แต่พลังนั้นก็มาพร้อม ปัญหาและกับดัก ที่ต้องระวัง:
- ความไม่คุ้นเคย — นักพัฒนาส่วนใหญ่ไม่ชินกับวิธีคิดแบบนี้ มันเป็นกระบวนทัศน์ (paradigm) ที่ต่างไปจากการเขียนทับสถานะที่เราคุ้นเคย จึงมีเส้นโค้งการเรียนรู้ที่ชัน แม้ผู้เขียนเองก็อดสงสัยไม่ได้ว่าความซับซ้อนที่คนมักบ่นถึงนี้ อาจมาจากการแยกส่วนที่ไม่ดีระหว่างส่วนที่ derive working copy กับส่วนที่ทำ domain logic มากกว่าจะเป็นข้อเสียโดยธรรมชาติของ event sourcing เอง
- การติดต่อกับระบบภายนอก — นี่คือกับดักใหญ่ที่สุด ถ้าตรรกะของเราทำงานที่มีผลกระทบต่อโลกภายนอก เช่น ส่งอีเมล หรือเรียก API ของระบบอื่น การ replay event ในภายหลังอาจไป กระตุ้นผลข้างเคียงเหล่านั้นซ้ำ โดยไม่ตั้งใจ (ส่งอีเมลซ้ำ สั่งของซ้ำ) เราจึงต้องออกแบบ gateway มาคั่นการติดต่อภายนอกให้แยกออกไป เพื่อให้ตอน replay ไม่ยิงผลข้างเคียงเหล่านั้นออกไปอีก และถ้าเราเรียกระบบภายนอกที่ให้คำตอบต่างกันในแต่ละเวลา (เช่น อัตราแลกเปลี่ยน) เราต้อง บันทึกคำตอบที่ได้รับไว้ด้วย เพื่อให้การ replay ยังคงให้ผลลัพธ์เดิม
- การจัดการ version ของ event (schema versioning) — เมื่อเวลาผ่านไป โครงสร้างของ event ย่อมเปลี่ยนแปลง แต่ event เก่าในอดีตยังอยู่ในบันทึก เราจึงต้องมีกลยุทธ์ให้ code version ใหม่ยังตีความ event รูปแบบเก่าได้ถูกต้อง
- ประสิทธิภาพในการสร้างสถานะใหม่ — เมื่อ event สะสมจนมีจำนวนมหาศาล การ replay ตั้งแต่ event แรกจะช้าลงเรื่อย ๆ ทางแก้ที่นิยมคือทำ snapshot เก็บภาพสถานะไว้เป็นระยะ แล้ว replay ต่อจาก snapshot ล่าสุดเท่านั้น
- การเปลี่ยนตรรกะที่ใช้ derive — ต้องระวังว่าถ้าตรรกะที่ใช้แปลง event ให้เป็นสถานะเปลี่ยนไป การ replay ประวัติเดิมด้วยตรรกะใหม่อาจให้ผลลัพธ์ที่ต่างจากเดิม เท่ากับ “ประวัติศาสตร์” ถูกตีความใหม่โดยไม่ได้ตั้งใจ
โดยสรุป event sourcing เป็น pattern ที่ทรงพลังแต่ซับซ้อน ควรใช้ด้วยความเข้าใจในกับดักของมัน และที่สำคัญ มันเป็น pattern ที่ เป็นอิสระจาก CQRS แม้ทั้งสองจะมักถูกพูดถึงคู่กันจนหลายคนเข้าใจผิดว่าเป็นเรื่องเดียวกัน
CQRS — การแยกความรับผิดชอบระหว่างการสั่งงานกับการสอบถาม
หัวข้อที่มีชื่อว่า “CQRS — การแยกความรับผิดชอบระหว่างการสั่งงานกับการสอบถาม”pattern สุดท้ายคือ CQRS ย่อมาจาก Command Query Responsibility Segregation (การแยกความรับผิดชอบระหว่างการสั่งงานกับการสอบถาม) แนวคิดคือการใช้ model ข้อมูลคนละตัวสำหรับการเขียน (command) กับการอ่าน (query) แทนที่จะมี model เดียวรับผิดชอบทั้งสองหน้าที่อย่างที่เราทำกันเป็นปกติ
ฝั่ง command จะเป็น model ที่ออกแบบมาเพื่อจัดการการเปลี่ยนแปลงสถานะ ส่วนฝั่ง query เป็น model ที่ออกแบบมาเพื่อตอบคำถามหรือแสดงผลโดยเฉพาะ
flowchart TB UI["Client"] -->|"commands"| CM["Command Model<br/>write side"] UI -->|"queries"| QM["Query Model<br/>read side"] CM -->|"update projections"| QM CM --> WS["Write Store"] QM --> RS["Read Store"]
ภาพ 4 — CQRS: ฝั่งเขียน (Command Model) กับฝั่งอ่าน (Query Model) ถูกแยกออกจากกันเป็น2 model การเปลี่ยนแปลงที่เกิดฝั่งเขียนจะถูก propagate ไปปรับปรุงมุมมองข้อมูล (projection) ที่ฝั่งอ่านใช้ ทั้งสองฝั่งอาจใช้ที่จัดเก็บข้อมูลคนละแบบที่เหมาะกับงานของตน ดูสรุปแบบอ้างอิงได้ที่หน้า CQRS
CQRS มักถูกใช้คู่กับ event sourcing เพราะเข้ากันได้ดี event store เหมาะกับการเขียนเชิง append ส่วนฝั่งอ่านก็สร้าง projection ที่ปรับแต่งมาเพื่อการ query โดยเฉพาะ แต่ขอย้ำอีกครั้งว่า ทั้งสองเป็นคนละ pattern ใช้ CQRS โดยไม่ทำ event sourcing ก็ได้ และทำ event sourcing โดยไม่ใช้ CQRS ก็ได้เช่นกัน
CQRS มีประโยชน์จริงในบางสถานการณ์ เช่น เมื่อ model สำหรับอ่านกับเขียนแตกต่างกันมากจนการยัดรวมเป็น model เดียวทำให้ทุกอย่างยุ่งเหยิง หรือเมื่อภาระงานฝั่งอ่านกับฝั่งเขียนไม่สมดุลกันอย่างรุนแรงจนต้องปรับขยาย (scale) แยกกัน
CQRS เพิ่ม ความซับซ้อนอย่างมีนัยสำคัญ ให้กับระบบ ด้วยเหตุนี้เพื่อนร่วมงานหลายคนของผู้เขียนจึงระแวดระวังการใช้ CQRS อย่างมาก เพราะเห็นว่ามันมักถูกนำไปใช้ผิดที่อยู่บ่อยครั้ง
หมายเหตุผู้แปล: หากจะนำ CQRS ไปใช้ คำแนะนำที่นิยมกัน (เป็นแนวทางเสริมจากฝั่ง DDD/DevIQ ไม่ใช่ถ้อยคำของ Fowler ในบทความนี้) คือจำกัดขอบเขตการใช้ไว้เฉพาะบาง Bounded Context ที่ประโยชน์คุ้มกับความซับซ้อนที่เพิ่มขึ้นจริง ๆ ไม่ใช่เหมาเอาไปครอบทั้งระบบ
การมองภาพรวมของ pattern เหล่านี้
หัวข้อที่มีชื่อว่า “การมองภาพรวมของ pattern เหล่านี้”เพื่อให้เห็นว่าการเหมารวม pattern เหล่านี้ก่อปัญหาจริงอย่างไร ผู้เขียนขอเล่าเหตุการณ์หนึ่ง
มีผู้จัดการ project คนหนึ่งซึ่งทั้งเก่งและมากประสบการณ์ เคยบอกกับผู้เขียนว่า event sourcing เป็น “หายนะ” ของ project เขา เพราะทุกครั้งที่จะแก้อะไรสักอย่าง งานกลับเพิ่มขึ้นเป็นสองเท่า เนื่องจากต้องไปอัปเดตทั้ง model ฝั่งอ่านและ model ฝั่งเขียน
แต่เพียงประโยคที่ว่า “ต้องอัปเดตทั้ง model ฝั่งอ่านและฝั่งเขียน” ก็ส่อเค้าความสับสนให้เห็นแล้ว เพราะการแยก model อ่านกับเขียนออกจากกันนั้นคือลักษณะของ CQRS ไม่ใช่ของ event sourcing ทั้งสองถูกจับมาปนกันตั้งแต่ต้น
และเมื่อผู้เขียนไปคุยกับ tech lead ของทีมเดียวกัน เขากลับชี้ว่าต้นตอที่แท้จริงคือการสื่อสารแบบ asynchronous ที่มีอยู่หนาแน่นในระบบต่างหาก ซึ่งเป็นตัวเพิ่มความซับซ้อนที่รู้กันดี แต่ก็ไม่ใช่สิ่งที่ผูกติดมากับ event sourcing หรือ CQRS โดยจำเป็นแต่อย่างใด
กล่าวคือความเจ็บปวดที่ทีมเจอถูกโยนไปโทษ pattern ผิดตัว เพียงเพราะทั้งสามแนวคิดถูกมัดรวมกันไว้ใต้ร่มคำเดียว
ผู้เขียนยอมรับตามตรงว่าอยากจะเขียนตำราฉบับสมบูรณ์สักเล่มที่สะสางความสับสนทั้งหมดนี้พร้อมวางแนวทางที่หนักแน่นเอาไว้ให้ แต่ก็ยังไม่มีเวลาพอจะทำเช่นนั้น บันทึกชิ้นนี้จึงเขียนขึ้นด้วยความหวังว่าจะเป็นประโยชน์บ้าง แม้จะรู้ตัวดีว่ามันยังห่างไกลจากสิ่งที่จำเป็นต้องมีจริง ๆ อยู่มาก
pattern ทั้งสี่ — Event Notification, Event-Carried State Transfer, Event Sourcing และ CQRS — เป็นแนวคิดที่แตกต่างกัน แม้จะถูกจับรวมไว้ใต้ร่มคำว่า “event-driven” อยู่บ่อยครั้ง ทั้งสี่สามารถหยิบมาใช้ร่วมกันหรือแยกกันก็ได้ และแต่ละอันก็มีชุดข้อดีข้อเสียเป็นของตัวเอง
ข้อคิดสำคัญที่ผู้เขียนอยากฝากไว้คือ เมื่อมีใครสักคนพูดว่า “เราควรทำระบบให้เป็น event-driven” สิ่งแรกที่ควรทำคือ ถามกลับให้ชัดว่าเขาหมายถึง pattern ตัวไหน เพราะการเหมารวมสี่แนวคิดนี้เข้าด้วยกันคือต้นตอของความคิดที่สับสนและการตัดสินใจเชิงสถาปัตยกรรมที่ผิดพลาด
การรู้ว่าเรากำลังใช้ pattern ใด และใช้เพราะเหตุผลอะไร ต่างหากที่ทำให้ประโยชน์ของแนวคิด event-driven ปรากฏออกมาได้จริง โดยไม่ตกหลุมพรางของความซับซ้อนที่ไม่จำเป็น
📄 อ่านต้นฉบับภาษาอังกฤษ: What do you mean by “Event-Driven”? — Martin Fowler, martinfowler.com (7 กุมภาพันธ์ 2017)