ข้าม​ไป​ยัง​เนื้อหา

เชื่อม Bounded Context ผ่าน HTTP ด้วย RPC และ REST

ใน​สอง​บท​ก่อนหน้า​เรา​เห็น​ว่า การ​เชื่อม Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design เข้า​ด้วย​กัน​ไม่ใช่​แค่​ทำให้ app คุย​กัน​ได้ แต่​ต้อง​ได้​ทั้ง scalability และ fault tolerance ด้วย บท​นี้​เสนอ​ทาง​เลือก​ที่​มัก​ถูก​มอง​ข้าม นั่น​คือ​การ​ใช้ HTTP — โพรโทคอล​ที่​อุปกรณ์​หลาย​พัน​ล้าน​เครื่อง​บน​อินเทอร์เน็ต​เข้าใจ​ตรง​กัน

HTTP เป็น​มาตรฐาน​ที่​รู้จัก​กัน​ดี เมื่อ​แต่ละ Bounded Context มา​จาก​ต่าง​ทีม​และ​ใช้​เทคโนโลยี​คนละ​ชุด​กัน HTTP จึง​น่า​สนใจ​เป็น​พิเศษ เพราะ​มัน​เป็น platform-agnostic — ไม่​ผูก​กับ​ภาษา​หรือ stack ใดๆ เรา​จะ​เทียบ​สอง​วิธี​หลัก​ของ​การ​เชื่อม​ผ่าน HTTP คือ RPC (ทั้ง​แบบ SOAP และ JSON) กับ event-driven REST แล้ว​ดู​ว่า​วิธี​ไหน​เหมาะ​กับ​สถานการณ์​ใด


ก่อน​จะ​ลง​รายละเอียด​เทคนิค มา​ดู​เหตุผล​ที่ HTTP คุ้ม​ค่า​ต่อ​การ​เชื่อม Bounded Context กัน​ก่อน

  • ไม่​ผูก​กับ platform (No Platform Coupling) — แต่ละ context สร้าง​ด้วย​เทคโนโลยี​อะไร​ก็ได้ ขอ​เพียง​เคารพ public contract ของ​ตัวเอง (คือ​รูปแบบ HTTP request/response) ทีม​ก็ refactor หรือ​เขียน​ใหม่​ด้วย​เทคโนโลยี​อื่น​ได้​อย่าง​อิสระ ตราบ​ใด​ที่ contract ยัง​เหมือน​เดิม การ​เชื่อม​ต่อ​กับ context อื่น​ก็​ไม่​พัง สิ่ง​นี้​สร้าง​ทั้ง loosely coupled application และ loosely coupled team
  • ทุก​คน​เข้าใจ HTTP — แทบ​ทุก​ภาษา​และ runtime มี library รองรับ HTTP เต็ม​ไป​หมด และ​เวลา​ขยาย​ทีม การ​หา dev ที่​เข้าใจ HTTP ง่าย​กว่า​หา​คน​ที่​เชี่ยวชาญ messaging framework เฉพาะ​ตัว​มาก
  • เครื่องมือ​และ library เติบโต​เต็ม​ที่ — เช่น Visual Studio ที่ generate proxy class ให้​อัตโนมัติ​เมื่อ​ชี้​ไป​ยัง web service ทำให้​เขียน code ที่ “ดูเหมือน” object-oriented ธรรมดา ทั้ง​ที่​เบื้องหลัง​คือ​การ​สื่อสาร​ข้าม​เครือข่าย
  • Dogfooding APIs — เมื่อ​ทุก​การ​สื่อสาร​วิ่ง​ผ่าน HTTP คุณ​อาจ​ไม่​ต้อง​แยก​ช่อง​ทาง internal/external แต่​ใช้ API ชุด​เดียวกัน ทั้ง​ภายใน​และ​ให้​บุคคล​ที่​สาม การ​ใช้ API ที่ share กับ​ลูกค้า​ด้วย​ตัวเอง​เรียก​ว่า dogfooding — ช่วย​ให้​เจอ​จุด​เจ็บ​ปวด​เดียว​กับ​ที่​ลูกค้า​เจอ (ต่าง​จาก messaging ที่​มัก​ใช้​ภายใน​เท่านั้น จึง​ต้อง​สร้าง API แยก​อีก​ชุด)
หัวใจ​ของ​บท​นี้

HTTP มอบ อิสระ​จาก​การ​ผูก​เทคโนโลยี ระหว่าง Bounded Context และ​ใช้ได้​สอง​แบบ​หลัก: RPC (เหมาะ​กับ​งาน​ง่าย เริ่มต้น​ได้​เร็ว) กับ event-driven REST (ให้ fault tolerance และ scalability ที่​ดี​กว่า) — ไม่​ว่า​จะ​เลือก​แบบ​ใด จง​ใช้​ทุก​โอกาส​ที่​เหมาะสม​เพื่อ ทำให้​แนวคิด​ของ domain ปรากฏ​ชัด ใน contract


ถ้า​ต้องการ​ระบบ​กระจาย​ที่​เชื่อม​ผ่าน HTTP ทาง​เลือก​แรก​คือ RPC (Remote Procedure Call) จุด​เด่น​ของ RPC คือ abstraction แบบ “ซ่อน​เครือข่าย” — การ​เรียก​ข้าม​เครื่อง​ดูเหมือน​การ​เรียก method ใน​เครื่อง​เดียวกัน เหมาะ​เมื่อ ความเร็ว​ใน​การ​พัฒนา​สำคัญ หรือ​ความ​ต้องการ​ด้าน scale ยัง​ไม่​สูง​นัก แต่​ข้อ​เสีย​คือ tight coupling ที่​ติด​มา​กับ RPC ทำให้ scale ยาก​และ​ทำ​ทีม​แบบหลวมๆ ได้​ลำบาก

สมมติ​สตาร์ท​อัพ​โซเชียลมีเดีย​ที่​กำลัง​เติบโต ปัจจุบัน​เป็น app ก้อน​เดียว​แบบ Big Ball of MudBig Ball of Mudระบบ​ที่​ปนเป​กัน​ยุ่งเหยิง​ไม่มี​ขอบเขต​ชัด ให้​ลาก​เส้น​ล้อม​ไว้​แล้ว​ยอมรับ​สภาพ ไม่​พยายาม​ทำ model ละเอียด​ใน​นั้น และ​ระวัง​ไม่​ให้​ลาม​ไป context อื่นStrategic Design ที่ Bounded Context เป็น​เพียง library ซึ่ง​ผูก​กัน​ด้วย binary dependency เมื่อ​แก้ context หนึ่ง​ก็​ทำให้​อีก context พัง​เป็น​ลูกโซ่ เรา​จะ​แก้​ด้วย​การ​แยก​แต่ละ context ออก​เป็น app เดี่ยว​ที่​สื่อสาร​กัน​ผ่าน HTTP เท่านั้น โดย​ใช้ RPC ข้าม​เครือข่าย​แทน​การ​เรียก method ภายใน process เพื่อ​ตัด binary dependency ทิ้ง​ไป

รูปที่ 13-1: use case "หา recommended users" — Discovery Bounded Context เรียกหา followers ของผู้ใช้ผ่าน RPC ข้ามเครือข่ายไปยัง Account Management Bounded Context แทนการเรียก method ใน process เดียวกัน

ใน​ระบบ​เดิม Discovery เรียก method FindUsersFollowers() ของ class FollowerDirectory ที่​อยู่​ใน Account Management โดยตรง — นี่​คือ binary dependency ที่​ผูก2 context ไว้​แน่น เรา​จะ​แทน​มัน​ด้วย RPC ชื่อ​เดียวกัน​ที่​วิ่ง​ข้าม​เครือข่าย

Recommender.cs (เดิม — เรียก​ใน process)
public static List<Account> FindRecommendedUsers(string accountId)
{
var fd = AccountManagement.FollowerDirectory;
// เรียกข้าม bounded context แบบ in-process — คือ binary dependency ที่เป็นปัญหา
var followers = fd.FindUsersFollowers(accountId);
return ApplyCleverRecommendationsAlgorithm(followers);
}

ทาง​เลือก​ดั้งเดิม​คือ SOAP (Simple Object Access Protocol) ที่​เพิ่ม layer อีก​ชั้น​บน HTTP โดย​บรรจุ​ข้อมูล​ไว้​ใน payload อย่าง​ละเอียด เช่น metadata ของ type และ function ทำให้​เครื่องมือ (tooling) พัฒนา​ไป​ได้​ไกล ตัวอย่าง​เช่น​ใน .NET ที่​ใช้ WCF ประกาศ RPC ได้​ด้วย​แอตทริบิวต์เพียง​ไม่​กี่​ตัว

IFollowerDirectory.cs
[ServiceContract] // บอก WCF ว่า class นี้มี RPC อยู่
public interface IFollowerDirectory
{
[OperationContract] // บอกว่า method นี้คือ RPC
List<Follower> FindUsersFollowers(string accountId);
}

WCF ร่วม​กับ Visual Studio จะ generate proxy class บน​ฝั่ง client ให้​อัตโนมัติ — “networking ที่​ได้​มาฟรีๆ” เช่น​นี้​คือ​เสน่ห์​ของ SOAP ฝั่ง Discovery เพียง​สร้าง proxy แล้ว​เรียก method ตาม​ปกติ การ​เรียก​นั้น​จะ​กลาย​เป็น RPC ข้าม​เครือข่าย​โดยที่ code แทบ​ไม่​ต่าง​จาก​ตอน​รัน​ใน​เครื่อง​เดียว

แต่ SOAP เสื่อม​ความ​นิยม​ลง​มาก เพราะ รูปแบบ​ข้อความ​ซับซ้อน​และ​เยิ่นเย้อ (XML envelope ที่​ยาวเหยียด) API สมัย​ใหม่​จึง​ไม่​ค่อย​สร้าง​ด้วย SOAP แล้ว ควร​ระมัดระวัง​หาก​จะ​เปิด public SOAP API แต่​หาก​ใช้​ภายใน​และ​เหมาะ​กับ​งาน ก็​ไม่ใช่​เรื่อง​น่า​กังวล​นัก

ทาง​เลือก​สมัย​ใหม่: plain XML หรือ JSON

ปัจจุบัน​นิยม​ทำ RPC over HTTP ด้วย payload เบาๆ อย่าง plain XML หรือ JSON แทน SOAP เช่น ASP.NET Web API ทำ JSON-RPC ได้​ง่าย — แลก​กับ​การ​ต้อง​เขียน code ทำ HTTP request และ parse response เอง​นิดหน่อย (ไม่มี proxy auto-generate เพราะ JSON ไม่มี metadata รวย​เท่า SOAP) แต่​ข้อมูล​ที่​วิ่ง​บน​สาย​สะอาด​และ​เบา​กว่า​มาก ทำให้ debug ง่าย​ขึ้น

ข้อ​จำกัด​ที่​ติด​มา​กับ RPC

ไม่​ว่า​จะ SOAP หรือ JSON-RPC ก็มี จุด​อ่อน​เชิง​โครงสร้าง เหมือน​กัน เพราะ temporal coupling (ทั้ง​สอง​ฝั่ง​ต้อง​ออนไลน์​พร้อม​กัน):

  • scale ยาก — ถ้า​โซ่ RPC พาด​ผ่าน 3 context อาจ​ต้อง scale ทั้ง 3 context พร้อม​กัน​เพื่อ​เร่ง​ความเร็ว
  • fault tolerance ต่ำ — ถ้า Account Management ล่ม Discovery ก็​ล่ม​ตาม เพราะ RPC ข้าม​ไป​ดึง followers ไม่​ได้

หลาย​บริษัท​ที่​มี​ความ​ต้องการ​ด้าน scale มหาศาล​ก็​ยัง​ใช้ RPC over HTTP เป็น​กลยุทธ์​หลัก​ได้ แต่​เมื่อ​ทีม​เติบโต​ขึ้น มัก​เริ่ม​เจอ​จุด​เจ็บ​ปวด — เวลา​ส่วน​ใหญ่​หมด​ไป​กับ​การ “ดับ​ไฟ” มากกว่า​การ​ส่ง​มอบ feature หลาย​ทีม​จึง​ค่อยๆ ย้าย​ไป​สู่​แนวทาง event-driven และ asynchronous

🚢 เทียบ​กับ​ระบบ​ขนส่ง: RPC คือ​การ​โทร​ถาม​แบบ​เรียลไทม์

ลอง​นึก​ว่า Booking Bounded Context ต้องการ​สถานะ​ล่าสุด​ของ Cargo จึง​ยิง RPC ไป​ถาม Tracking Bounded Context ทุก​ครั้ง​ที่​ลูกค้า​เปิด​หน้า​จอ ถ้า Tracking ล่ม หน้า​จอ booking ก็​ค้าง​ตาม​ทันที — และ​หาก​วัน​คริสต์มาส​มี​คน​เช็ก​สถานะ​พร้อม​กัน​เป็น​ล้าน ก็​ต้อง scale ทั้ง2 context ไป​พร้อม​กัน นี่​คือ temporal coupling ใน​สนาม​จริง


REST เป็น​คำ​ที่​ถูก​เข้าใจ​ผิด​และ​ใช้​ผิด​กัน​บ่อย​มาก ก่อน​สร้าง​ระบบ RESTful ใดๆ จึง​ต้อง​เข้าใจ​ก่อน​ว่า​แท้จริง​แล้ว REST คือ​อะไร Roy Fielding เป็น​ผู้​แนะนำ REST สู่​โลก​ใน​ฐานะ architectural style ที่​อิง​หลักการ​ซึ่ง​ทำให้​อินเทอร์เน็ต​ประสบ​ความ​สำเร็จ

REST APIs must be hypertext-driven. — Roy Fielding (2008)

แก่น​ของ REST มี​สาม​เรื่อง:

  • Resources — request ใน​ระบบ RESTful คือ​การ​ขอ “ทรัพยากร” (resource) เช่น​เอกสาร หน้า​เว็บ หรือ​สื่อ เข้า​กัน​ได้​ดี​กับ DDD เพราะ​แนวคิด​ใน domain แสดง​เป็น resource ได้ ช่วย​เผยแพร่ Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Design เช่น B2bTransactions หรือ PersonalTransactions ปรากฏ​เป็น URI ตรงๆ — ต่าง​จาก RPC ที่​ตั้ง​ชื่อ​แบบ​คำ​สั่ง (imperative) เลียน​แบบ​การ​เรียก method เพิ่มเติม resource หนึ่ง​มี​ได้​หลาย representation (JSON, XML, HTML) โดย client เลือก​ผ่าน header Accept
  • Hypermedia — มนุษย์​ท่อง​เว็บ​ด้วย​การ​คลิก link เครื่อง​ก็​ทำได้​เช่น​กัน โดย​ฝัง hyperlink ไว้​ใน resource แล้ว​เดิน​จาก resource หนึ่ง​ไป​อีก resource ด้วย​การ​ตาม link สิ่ง​นี้​ทำให้ client ไม่​ผูก​กับ URI — client และ server วิวัฒน์​อิสระ​จาก​กัน​ได้ (ต่าง​จาก SOAP ที่​มัก​เปราะ​เพราะ client/server ผูก​กัน​แน่น) และ​ยัง​เป็น​โอกาส​แสดง Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Design ผ่าน link ที่​แทน “ขั้นตอน​ถัด​ไป” ของ workflow
  • Statelessness — application state (เช่น ของ​ใน​ตะกร้า​สินค้า) ไม่​ควร​เก็บ​ไว้​บน server แต่​เก็บ​ไว้​ฝั่ง client แล้ว​ส่ง​มา​ทุก​ครั้ง​ที่ server ต้อง​ใช้ ทำให้​เมื่อ server เครื่อง​หนึ่ง​มี​ปัญหา เครื่อง​อื่น​ก็​เข้า​มา​รับช่วง​ต่อ​ได้​ทันที — นี่​คือ​รากฐาน​ของ fault tolerance และ scalability

REST อิง​หลักการ​ที่​ทำให้​เว็บ​สำเร็จ จึง​โอบ​รับ convention ของ HTTP อย่าง​เต็ม​ที่:

  • VerbsGET อ่าน resource, DELETE ลบ, PUT สร้าง​ที่ URI ที่​ต้องการ, POST เพิ่ม​สมาชิก​เข้า collection การ​มี​ชุด​กริยา​เดียว​ที่​ใช้​เหมือน​กัน​ทั่ว​อินเทอร์เน็ต ทำให้​สร้าง generic client และ infrastructure (เช่น cache) ที่​เข้าใจ convention ได้​ง่าย
  • Status codes — จัด​กลุ่ม​ตาม​เลข​ตัว​แรก: 2xx สำเร็จ, 3xx redirect, 4xx client ผิด, 5xx server ผิด — agent ใด​ก็ตาม​บน​เว็บ​เข้าใจ convention เดียวกัน
  • Headers — ใช้ caching header (สำคัญ​ต่อ scalability ของ REST) และ​เพราะ REST เป็น stateless จึง​นิยม​ส่ง​ข้อมูล authentication/authorization ผ่าน header ด้วย​โพรโทคอลอย่าง OAuth
REST ไม่ใช่​แค่ HTTP + JSON

ก่อน​เรียก API ของ​คุณ​ว่า RESTful ให้​เช็กอย่าง​น้อย​ว่า​มัน​มี hypermedia และ resources เป็น​ศูนย์กลาง​หรือ​ไม่ Richardson Maturity Model (โดย Leonard Richardson, เผยแพร่​โดย Martin Fowler) เป็น “บารอมิเตอร์” วัด​ว่า API ของ​คุณ​ใกล้​เป็น REST แท้​แค่​ไหน — หลาย​ระบบ​ที่​เรียก​ตัวเอง​ว่า REST จริงๆ แค่​ทำ RPC over HTTP


เมื่อ​สตาร์ท​อัพ​เติบโต​จน​จำนวน​ผู้​ใช้​พุ่ง​ทวีคูณ และ​ธุรกิจ​ต้องการ​เพิ่ม premium account ที่​โปรโมต​ให้​ผู้​ใช้​ทั่วไป การ​เชื่อม​แบบ RPC เดิม​ก็​เริ่ม scale ไม่​ไหว เรา​จึง​ออกแบบ​ใหม่​เป็น event-driven architecture ที่​ใช้ REST และ HTTP เพื่อ​เลี่ยง​การ​ผูก​ติด​กับ​เทคโนโลยี​หรือ​ผู้​ขาย (vendor lock-in) ตลอด​อายุ​ระบบ

ขั้น​แรก​คือ เริ่ม​ที่ domain — ร่าง​กระบวนการ​ธุรกิจ (business process) ด้วย Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Design ก่อน​ตัดสิน​ใจ​เรื่อง​เทคนิค รูป​ที่ 13-9 เป็น component diagram ที่​โฟกัส​ที่ domain command และ domain event (จึง​ใช้ได้​กับ​ทั้ง messaging และ REST เพราะ​อิสระ​จาก​เทคโนโลยี)

รูปที่ 13-9: component diagram ของ use case "Recommended Accounts" — สื่อสารด้วย domain event สองตัวคือ "Began Following" (เมื่อบัญชีหนึ่งเริ่มติดตามอีกบัญชี) และ "Premium Recommendations Identified" ที่ Discovery ระบุบัญชี premium ที่ผู้ใช้น่าจะชอบ

Began Following และ Premium Recommendations Identified คือ Domain EventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design ที่​เป็น​ส่วน​หนึ่ง​ของ Ubiquitous LanguageUbiquitous Languageภาษา​กลาง​ที่​ทุก​คนใน​ทีม (ทั้ง​นัก​พัฒนา​และ​ผู้เชี่ยวชาญ​ธุรกิจ) ใช้​ร่วม​กัน อิง​กับ domain model โดยตรง คำ​เดียวกัน​ต้อง​หมาย​ถึง​สิ่ง​เดียวกัน และ​สะท้อน​ลง​ใน codeStrategic Design — ทุก​คนใน​บริษัท​เข้าใจ​ตรง​กัน และ​เป็น​แนวคิด​แกน​ของ​ธุรกิจ เมื่อ event เหล่า​นี้​วิ่ง​ข้าม Bounded Context มัน​ก็​ทำ​หน้าที่​เป็น Integration EventIntegration Eventevent ที่​สื่อสาร “ข้าม” Bounded Context หรือ​ข้าม microservice ผ่าน event bus — ต่าง​จาก Domain Event ที่​ทำงาน​ภายใน context เดียว (in-process)Architecture

หลักการ​อื่น​ที่​นำ​มา​ใช้:

  • SOA — แยก​แต่ละ Bounded Context ให้​ทีม​เดียว​เป็น​เจ้าของ ทำให้ loose coupling อยู่​ใน​เอื้อม​มือ สิ่ง​ที่​เปลี่ยน​ไป​เมื่อ​ใช้ SOA ร่วม​กับ HTTP คือ contract ระหว่าง​ทีม​ไม่ใช่ class ใน code อีก​ต่อ​ไป แต่​เป็น​รูปแบบ HTTP request/response
  • Event-driven และ reactive — HTTP ไม่​รองรับ publish/subscribe มา​แต่​กำเนิด จึง​ไม่​สามารถ push event ออก​ไป​หา subscriber ได้​ทันที​แบบ message bus ทางออก​ของ REST คือ​ให้ client poll เพื่อ​ตรวจ​หาความ​เปลี่ยนแปลง แม้​การ poll มัก​ถูก​มอง​ว่า scale ได้​ไม่​ดี แต่​การ​ใช้ caching convention ของ HTTP ก็​ช่วย​ลบล้าง​ปัญหา​นี้​ได้

รูปที่ 13-10: containers diagram ของ Discovery, Account Management และ Marketing Bounded Context — แต่ละ component เล็กพอจะ scale อิสระ ไม่ share ฐานข้อมูล แต่ละทีมเลือกเทคโนโลยีเองได้ (เช่น .NET กับ Scala/Play) สื่อสารผ่าน HTTP และ Atom feed

ข้อ​พิจารณา​สำคัญ​ต่อ scalability คือ granularity ของ project คำ​แนะนำ​เบื้องต้น​ใน​บท​นี้​คือ​เริ่ม​ที่ 1 project ต่อ1 resource เพื่อ​ให้ deploy และ scale แต่ละ resource ได้​อย่าง​อิสระ​ตาม​ความ​ต้องการ​ของ​มัน

🛒 เทียบ​กับ e-commerce: Order Placed เป็น event ข้าม context

ใน​ระบบ e-commerce เมื่อ Ordering Bounded Context บันทึก OrderPlaced มัน​สามารถ publish event นั้น​เป็น Atom feed ส่วน Shipping และ Notification Bounded Context ก็ poll feed นั้น​แล้ว​ทำงาน​ต่อตาม​จังหวะ​ของ​ตัวเอง — ถ้า Notification ล่ม​ไป​ชั่ว​ครู่ Ordering ก็​ยัง​รับ order ใหม่​ได้​ตาม​ปกติ ไม่​ผูก​ชะตา​กัน​แบบ RPC


Hypermedia คือ​หัวใจ​ของ REST เพราะ​มัน​ทำให้ client/server วิวัฒน์​อิสระ แต่ client ต้อง​รู้​บาง​อย่าง​ล่วงหน้า — นั่น​คือ entry point resource ที่ client ควร​ผูก​ตัวเอง​ไว้ จาก​นั้น​แค่​ตาม link ใน hypermedia ที่​ตอบ​กลับ​มา

รูปที่ 13-11: ลำดับ HTTP request ของ use case "Add Follower" — client ผูกแค่ URI ของ entry point (/accountmanagement) แล้วตาม link ใน hypermedia ไปทีละขั้นจนถึง resource followers ที่ POST เพื่อเพิ่มผู้ติดตาม

client ผูก​แค่ URI ของ entry point เท่านั้น จาก​นั้น​ตาม link ที่​ส่ง​กลับ​มา​เป็น HTTP response จนถึง Followers resource การ​ออกแบบ​แบบ​นี้​ทำให้ Account Management เปลี่ยน URI ภายใน​ได้​โดย​ไม่​ทำ client พัง

  • HAL (Hypertext Application Language) — คือ JSON/XML ธรรมดา​ที่​เพิ่ม convention สำหรับ​แทน hypermedia link link ทั้งหมด​อยู่​ใต้ _links ที่ root แต่ละ link ขึ้น​ต้น​ด้วย relation (เช่น self, accounts) และ​มี href มี​แค่ link self ที่​บังคับ ที่​เหลือ optional
entry point resource (HAL+JSON)
{
"_links": {
"self": { "href": "http://localhost:4100/accountmanagement" },
"accounts": { "href": "http://localhost:4101/accountmanagement/accounts" },
"beganfollowing": { "href": "http://localhost:4102/accountmanagement/beganfollowing" }
}
}
  • Atom (application/atom+xml) — มาตรฐาน​สร้าง RSS feed จึง​เหมาะ​เป็น “feed ของ event” การ​ใช้ Atom feed เป็นกระแส​ของ event คือ building block หลัก ของ​การ​สร้าง​ระบบ event-driven แบบ​กระจาย​ด้วย REST ใน​บท​นี้

อีก​เทคนิค​ที่​ช่วย​แก้​ปัญหา “การนำทาง​ที่​ไม่มี​ประสิทธิภาพ” (ต้อง​ตาม link เป็น​ร้อย​กว่า​จะ​ถึง resource ที่​ต้องการ) คือ URI template — link ที่​มี templated: true พร้อม placeholder เช่น accounts/{accountId} ให้ client แทน​ค่า ID ที่​ต้องการ​เอง ลด​จาก​ร้อย request เหลือ​เพียง​ครั้ง​เดียว

อย่า​ผูก model-binding เข้า​กับ domain class โดยตรง

ใน​ตัวอย่าง หนังสือ bind request body เข้า​กับ class BeganFollowing ที่​เป็น​ทั้ง domain event และ​โครงสร้าง​เก็บ​ข้อมูล เพื่อ​ความ​กระชับ — แต่​ใน​ระบบ production จริง ไม่​แนะนำ เพราะ​ทำให้ contract ภายนอก​ผูก​แน่น​กับ model ภายใน เปลี่ยน model ที​ไร​อาจ​ทำ client พัง ควร​มี​ชั้น​แปลง (เทียบ​กับ​แนวคิด Anticorruption LayerAnticorruption Layer (ACL)ชั้น​กั้น​ที่​ทีม​ปลาย​น้ำ​สร้าง​ขึ้น​เพื่อ​แปลง model ของ​ระบบ​อื่น​ให้​เป็น​ภาษา​ของ model ตัวเอง ป้องกัน​ไม่​ให้ model ภายนอก​มา “ปน​เปื้อน” model เราStrategic Design / Open Host ServiceOpen Host Service (OHS)ทีม​ต้นน้ำ​เปิด​บริการ​เป็น​โพรโทคอล/API ที่​นิยาม​ไว้​ชัดเจน ให้​ทุก​คน​ที่​ต้อง​เชื่อม​ต่อ​มา​ใช้ได้ เหมาะ​กับ​ระบบ​ที่​มี​ผู้​ใช้​หลาย​ฝ่ายStrategic Design)


หัวใจ​ของ event-driven REST คือ​วงจร​สาม​ส่วน: บันทึก event → publish เป็น feed → consumer poll และ​ประมวล​ผล

1. Persist event — เมื่อ​มี POST เข้า​มา​ที่ followers resource controller จะ​สร้าง domain event แล้ว​บันทึก​ลง Event Store (เครื่องมือ​ของ Greg Young) โดย append เข้า stream ชื่อ BeganFollowing

FollowersController.cs (POST → persist event)
[HttpPost]
public IHttpActionResult IndexPost(string accountId, Follower follower)
{
var evnt = new BeganFollowing
{
AccountId = accountId,
FollowerId = follower.AccountId
};
EventPersister.PersistEvent(evnt); // append เข้า stream "BeganFollowing"
return RedirectToRoute("Account Followers", new { accountId });
}

2. Publish เป็น Atom feed — อีก component ดึง event ล่าสุด​จาก Event Store มา​แปลง​เป็น​รายการ​ใน SyndicationFeed แล้ว​ตอบ​กลับ​ด้วย content type application/atom+xml component ที่ publish event เป็น Atom feed เหมือน message-publishing component ใน​ระบบ messaging

Archiving feeds — กัน​ไม่​ให้ feed บวม

ใน​ระบบ scale สูง​ที่​มี event เป็น​พัน​ต่อ​วินาที feed เดียว​จะ​ใช้งาน​ไม่​ได้​และ​เปลือง bandwidth มหาศาล ทางออก​คือ​กำหนด​จำนวน event ต่อ feed แล้ว archive เมื่อ​เต็ม โดย​แต่ละ feed มี hypermedia link ไป​ยัง archive ก่อนหน้า/ถัด​ไป (ดู IETF RFC 5005 “Feed Paging and Archiving”)

3. Consume ด้วย​การ poll — การ consume Atom feed ที่​เปิดเผย domain event ก็​เหมือน​การ subscribe ข้อความ​ใน​ระบบ messaging แต่​กลับ​ด้าน​จาก “ถูก push” เป็น “poll แล้ว​ดึง​เอง” logic ทั่วไป​มี​สาม​ขั้น:

  1. ดึง event ชุด​ถัด​ไป เริ่ม​จาก ID ของ event ล่าสุด​ที่​ประมวล​ผล​แล้ว (หรือ​ตัว​แรก​ถ้า​ยัง​ไม่​เคย​ทำ)
  2. ประมวล​ผล​แต่ละ event ตาม domain policy
  3. เก็บ ID ของ event ล่าสุด​ที่​ประมวล​ผล​ไว้
BeganFollowingPollingFeedConsumer.cs (วน loop poll)
while (true)
{
var atomFeed = FetchFeed(); // ตาม link ใน hypermedia จาก entry point → feed
var unprocessed = GetUnprocessedEvents(atomFeed.Items.ToList());
if (unprocessed.Any())
ProcessEvents(unprocessed); // ใช้กฎ domain ที่นี่ แล้วจำ ID ล่าสุด
Thread.Sleep(1000); // poll สูงสุดวินาทีละครั้ง
}

สังเกต​ว่า consumer ผูก​แค่ URI ของ entry point แล้ว​ตาม _links["beganfollowing"] ไปหา feed เอง — client/server จึง decouple กัน​จริง


Versioning — การ​ปรับปรุง​เล็กๆ ทำได้​โดย​ไม่​ทำ client พัง ถ้า​เปลี่ยน​แบบ backward-compatible เช่น​เพิ่ม field ใหม่​ต่อ​ท้าย resource (client เดิม​ที่​ผูก​กับ format เก่า​ไม่​พัง) ส่วน​การ​รื้อ​ใหญ่ (breaking change) นิยม​ใส่ version ใน URI (/v2/accountmanagement/) หรือ​ใน HTTP header (Version: 2)

Monitoring — ข้อดี​ของ HTTP คือ​มี​เครื่องมือ monitoring สำเร็จรูป​เสียบ​ใช้ได้​ทันที (เช่น New Relic) หรือ​เก็บ custom metric เอง (เช่น StatsD)

แต่ REST ก็​มี​ข้อ​จำกัด​เมื่อ​เทียบ​กับ messaging — ส่วน​ใหญ่​คือ ต้อง​ลงแรง​พัฒนา​มาก​ขึ้น​ตอน​เริ่มต้น เพื่อ​ชดเชย feature ที่ messaging มี​มา​ให้​พร้อม:

ข้อ​จำกัด​ของ event-driven REST
  • Fault tolerance น้อย​กว่า out-of-the-box — ถ้า Event Store ล่ม​ตอน​พยายาม​เก็บ event จะ ไม่มี​การ​กู้​คืน​อัตโนมัติ เมื่อ​มัน​กลับ​มา (ต่าง​จาก messaging ที่ retry ข้อความ​ให้) ต้อง​เสริม store-and-forward, queue หรือ load balancer/cluster เอง
  • Eventual ConsistencyEventual Consistencyความ​ถูกต้อง​ที่ “ตาม​มา​ทีหลัง” ไม่ใช่​ทันที ใช้​กับ​การ​อัปเดต​ข้าม Aggregate ผ่าน Domain Event แทน​การ​บังคับ​ให้​ตรง​กัน​ใน​ทันที​ภายใน transaction เดียวTactical Design — ระบบ share-nothing ที่​สื่อสาร async ย่อม​เจอ eventual consistency สูง consumer ที่ poll feed จะ​ยัง​ไม่​เห็น​ข้อมูล​ใหม่​จนกว่า​จะ poll และ​ประมวล​ผล​รอบ​ถัด​ไป — รับมือ​ด้วย​การ​ใช้ transaction เล็ก​ลง, roll-forward ไป​สู่​สถานะ​ใหม่ และ retry หลาย​ครั้ง

แต่​ข้อ​เสีย​เหล่า​นี้​อาจ​กลาย​เป็น​ข้อ​ได้​เปรียบ​ตลอด​อายุ project — คุณ​มี framework ที่​ต้อง​ดูแล​น้อย​ลง และ​เข้าใจ​กลไก​การ​สื่อสาร​ของ​ระบบ​กระจาย​ของ​ตัวเอง​อย่าง​ลึกซึ้ง​ขึ้น

สรุป​ทาง​เลือก RPC vs REST
  • RPC (SOAP/JSON) — เริ่ม​เร็ว เหมาะ​กับ​งาน​ง่าย แต่ tight coupling ทำให้ scale ยาก​และ fault tolerance ต่ำ (temporal coupling)
  • Event-driven REST — อิง resource, hypermedia, statelessness และ Atom feed ของ domain event มอบ scalability และ fault tolerance เทียบเท่า messaging โดย​พึ่ง​เพียง HTTP — แลก​กับ​งาน​พัฒนา​ใน​ช่วง​เริ่มต้น​ที่มากกว่า
  • ทั้ง​สอง​แบบ: HTTP request/response คือ contract ระหว่าง Bounded Context จง​หลีก​เลี่ยง breaking change และ​ทำให้​แนวคิด​ของ domain ปรากฏ​ชัด​ใน​สัญญา​นั้น​เสมอ

🔗 อ้างอิง​เพิ่มเติม​ใน DevIQ

เจาะ​ลึก​แนวคิด​ใน​บท​นี้​ต่อ​ได้ที่​คลัง​อ้างอิง DevIQ:

  • Anti-Corruption Layer — คำ​เตือน​เรื่อง​อย่า​ผูก model-binding เข้า​กับ domain class โดยตรง​ใน​บท​นี้ คือ​การ​ใช้​แนวคิด ACL เพื่อ​กัน​ไม่​ให้ contract ภายนอก​รั่ว​เข้าไป​ผูก​กับ model ภายใน
  • Context Mapping — การ​เลือก​เชื่อม Bounded Context ด้วย RPC หรือ event-driven REST คือ​ส่วน​หนึ่ง​ของ​การ​วาด Context Map ที่​กำหนด​ว่า​แต่ละ context สัมพันธ์​และ​สื่อสาร​กัน​อย่างไร
  • Domain EventsBegan Following และ Premium Recommendations Identified ที่ publish ผ่าน Atom feed ใน​บท​นี้ คือ​ตัวอย่าง​ของ Domain Event ที่​ทำ​หน้าที่​เป็น Integration Event ข้าม Bounded Context

เช็กความเข้าใจ — บทที่ 13

ข้อ 1 / 4

เหตุผลหลักที่ HTTP น่าสนใจสำหรับการเชื่อม Bounded Context คืออะไร?