ทำไมต้องแยก context
ตลอดคอร์สที่ผ่านมาในเว็บนี้ เราปั้น domain ฟู้ดเดลิเวอรีตัวเดียวกันมาตลอด — เริ่มจาก Event Storming ที่ค้น domain ทั้งทีมด้วยผนัง Post-it ต่อด้วยการวางสถาปัตยกรรมและปั้น model Order ให้ถือกฎธุรกิจไว้ใน code แล้วพิสูจน์ว่ามันทำงานถูกด้วย test ทั้งพีระมิด ทุกคอร์สที่ผ่านมามี Order แค่ก้อนเดียว และนั่นก็เพียงพอ คอร์สนี้เดินต่อจากจุดเดิม แต่ถามคำถามที่ใหญ่ขึ้น: เมื่อ Order ก้อนเดียวเริ่มแบกความหมายของคนหลายฝ่ายไม่ไหว เราจะแยกมันอย่างไร?
code เต็มของคอร์สนี้อยู่ที่ repo kaen-food-ordering (กำลังจัดทำ) — คอร์สนี้ต่อยอด domain Order เดิมให้กลายเป็น modular monolith 4 module บทนี้เป็นภาคแนวคิดล้วนๆ ยังไม่แตะโครง code จริง (เริ่มลง code ในบทถัดๆ ไป)
กับดัก “model เดียวครองทุกอย่าง”
หัวข้อที่มีชื่อว่า “กับดัก “model เดียวครองทุกอย่าง””ตอนระบบยังเล็ก การมี Order class เดียวที่ทุกส่วนใช้ร่วมกันดูเป็นเรื่องดี — มีที่เดียวให้แก้ ไม่ซ้ำซ้อน แต่พอ feature โต Order ก้อนนั้นจะค่อยๆ บวมขึ้นต่อเนื่อง เพราะแต่ละฝ่ายอยากให้มันตอบโจทย์ของตัวเอง:
- ครัว อยากรู้ว่าต้องทำเมนูอะไร กี่จาน อยู่สเตชันไหน ทำเสร็จหรือยัง
- ไรเดอร์ อยากรู้ว่าไปรับที่ไหน ส่งที่ไหน เส้นทางเป็นยังไง ถึงหรือยัง
- บิลลิ่ง อยากรู้ว่าเก็บเงินสำเร็จไหม ยอดเท่าไร ถ้ายกเลิกต้องคืนกี่บาท
ทุกความอยากรู้พวกนี้ไปลงเอยที่ Order ตัวเดียวกัน กลายเป็น god object ที่พองจนไม่มีใครเป็นเจ้าของความถูกต้องของมัน:
public class Order{ // มุมของ Ordering public List<OrderLine> Items { get; set; } public Money Total { get; set; }
// มุมของ Kitchen แอบเข้ามา public string KitchenStation { get; set; } public bool IsCooking { get; set; }
// มุมของ Delivery แอบเข้ามาอีก public string RiderId { get; set; } public Route DeliveryRoute { get; set; }
// มุมของ Payment แอบเข้ามาอีก public string PaymentStatus { get; set; } public Money? RefundAmount { get; set; }}ปัญหาไม่ใช่แค่ class ยาว แต่คือ model เดียวที่พยายามหมายถึงทุกอย่าง สุดท้ายไม่แม่นกับใครเลย ทุก field ที่ฝ่ายหนึ่งเพิ่มเข้ามา บังคับให้อีกสามฝ่ายต้องแบกสิ่งที่ตัวเองไม่เกี่ยว กฎของครัว ไรเดอร์ และบิลลิ่งปนกันอยู่ใน class เดียวจนไม่มีขอบเขตความสอดคล้อง (consistency boundary) ที่ชัดเจน แก้จุดหนึ่งกระเทือนอีกสามจุดโดยไม่ตั้งใจ
ทางออกไม่ใช่ทำ Order ให้ฉลาดขึ้นหรือซอยเป็น field ย่อยๆ เพิ่ม แต่คือ ยอมรับว่ามันควรมีหลายตัว — แต่ละตัวอยู่ใน Bounded ContextBounded Contextขอบเขตที่ ubiquitous language และ model หนึ่งชุดมีความหมายคงเส้นคงวา นอกขอบเขตนี้คำเดียวกันอาจหมายถึงคนละอย่าง เช่น 'order' ใน Ordering กับ 'ticket' ใน Kitchen คือมุมมองของสิ่งเดียวกันคนละ modelStrategic Design ของตัวเอง ขอบเขตที่ภาษาและ model หนึ่งชุดมีความหมายคงเส้นคงวา นอกขอบเขตนั้นคำเดียวกันมีสิทธิ์หมายถึงคนละอย่าง
Subdomain กับ Bounded Context — คนละชั้นของปัญหา
หัวข้อที่มีชื่อว่า “Subdomain กับ Bounded Context — คนละชั้นของปัญหา”ก่อนจะลากเส้นแบ่ง ต้องแยกให้ออกก่อนว่ามีคำสองคำที่คนมักปนกัน
SubdomainSubdomainส่วนย่อยของปัญหาทางธุรกิจ (problem space) แบ่งเป็น core (Ordering — จุดได้เปรียบ), supporting (Kitchen, Delivery — จำเป็นแต่ไม่ใช่จุดขาย), generic (Payment — ใช้ของสำเร็จรูปได้) คนละเรื่องกับ Bounded Context ซึ่งเป็นคำตอบ (solution space)Strategic Design คือส่วนย่อยของ ปัญหาทางธุรกิจ (problem space) — เป็นเรื่องขององค์กร ไม่ใช่ของ code แบ่งได้สามแบบตามความสำคัญเชิงธุรกิจ:
- Core — จุดได้เปรียบที่ทำให้เราชนะคู่แข่ง สำหรับฟู้ดเดลิเวอรีคือ Ordering (ประสบการณ์สั่งอาหารที่ลื่นและแม่น) ตรงนี้ต้องลงแรงปั้นเองให้ดีที่สุด
- Supporting — จำเป็นแต่ไม่ใช่จุดขาย ได้แก่ Kitchen (คิวทำอาหาร) และ Delivery (การจัดส่ง) ต้องมี แต่ไม่ใช่สิ่งที่ลูกค้าเลือกเราเพราะมัน
- Generic — ปัญหาที่ใครๆ ก็มี ใช้ของสำเร็จรูปได้ เช่น Payment (รับเงิน/คืนเงิน) ไม่มีเหตุผลจะปั้น payment gateway เอง
Bounded Context เป็นคนละชั้น — มันคือขอบเขตของ คำตอบ (solution space) เส้นที่เราลากในซอฟต์แวร์จริงๆ ว่า model ชุดหนึ่งกับภาษาชุดหนึ่งจะคงความหมายไว้ตรงไหน โดยอุดมคติ1 subdomain จับคู่กับ1 bounded context พอดี — Ordering subdomain ได้ Ordering context, Payment subdomain ได้ Payment context — แต่ในระบบจริงมันไม่ตรงกันเป๊ะเสมอไป จุดที่ต้องจำคือ subdomain เป็นเรื่องที่ธุรกิจแบ่งให้เรา ส่วน bounded context เป็นเส้นที่เราเลือกลากเอง
Ubiquitous Language ผูกกับ context ไม่ใช่ทั้งองค์กร
หัวข้อที่มีชื่อว่า “Ubiquitous Language ผูกกับ context ไม่ใช่ทั้งองค์กร”เส้นที่แบ่ง context จริงๆ ไม่ใช่เส้นเทคนิค แต่คือ จุดที่ภาษาเปลี่ยน Ubiquitous LanguageUbiquitous Languageภาษากลางที่ทีมและผู้เชี่ยวชาญธุรกิจใช้ร่วมกัน แต่ยึดกับ context เดียว ไม่ใช่ทั้งองค์กร — คำว่า order สื่อความหมายต่างกันในปากคนของ Ordering, Kitchen และ Delivery แม้พูดถึงเหตุการณ์เดียวกันStrategic Design คือภาษากลางที่ทีมและผู้เชี่ยวชาญธุรกิจใช้ร่วมกัน แต่มันยึดกับ context เดียว ไม่ใช่ทั้งบริษัท — ความเข้าใจผิดที่พบบ่อยคือพยายามหาคำนิยาม “ออเดอร์” ที่ทุกคนทั้งองค์กรเห็นตรงกัน ซึ่งเป็นไปไม่ได้และไม่จำเป็น
คำว่า “order” ปรากฏในทุกฝ่าย แต่หมายถึงคนละสิ่ง แม้พูดถึงออเดอร์ใบเดียวกัน:
- ใน Ordering — “order” คือ ตะกร้า + รายการอาหาร + ราคารวม สนใจว่าลูกค้าเลือกอะไร ยืนยันหรือยัง
- ใน Kitchen — “order” คือ ตั๋วคิวทำอาหาร (ticket) สนใจว่าทำเมนูอะไร ลำดับไหน เสร็จหรือยัง ไม่แคร์ราคาเลย
- ใน Delivery — “order” คือ งานรับ-ส่ง (assignment) ที่มีต้นทางปลายทาง สนใจระยะทางและเวลา ไม่แคร์ว่าในกล่องมีผัดกะเพราหรือข้าวมันไก่
- ใน Payment — “order” คือ ยอดที่ต้องเก็บหรือคืน (charge) สนใจแค่จำนวนเงินและสถานะการชำระ
พอภาษาต่างกันขนาดนี้ การยัดทั้งสี่มุมมองลง class เดียวจึงไม่ใช่การประหยัด แต่คือการบังคับให้สี่ภาษามาเบียดกันในกล่องเดียว แต่ละ context ควรมี Order version ของตัวเองที่ถือเฉพาะข้อมูลและกฎที่ตัวเองต้องรักษา
flowchart LR
subgraph OLD["แบบเดิม: model เดียวครองทุกอย่าง"]
GOD["Order ก้อนเดียว<br/>items, total, kitchenStation,<br/>riderId, route,<br/>paymentStatus, refundAmount ..."]
K0["ครัวอยากได้ ticket"]
D0["ไรเดอร์อยากได้ assignment"]
B0["บิลลิ่งอยากได้ charge"]
K0 --- GOD
D0 --- GOD
B0 --- GOD
end
subgraph NEW["แบบใหม่: 4 Bounded Context แต่ละอันมี order ของตัวเอง"]
ORD["Ordering<br/>Order = ตะกร้า + รายการ + ราคา"]
KIT["Kitchen<br/>Ticket = คิวทำอาหาร"]
DEL["Delivery<br/>Assignment = งานรับ-ส่ง"]
PAY["Payment<br/>Charge = ยอดเก็บ/คืน"]
end
OLD ==>|"แยกตามจุดที่ภาษาเปลี่ยน"| NEW
classDef god fill:#dc2626,stroke:#7f1d1d,color:#f8fafc;
classDef want fill:#fca5a5,stroke:#7f1d1d,color:#1a1a1f;
classDef ctx fill:#c7d2fe,stroke:#3730a3,color:#1a1a1f;
class GOD god;
class K0,D0,B0 want;
class ORD,KIT,DEL,PAY ctx;
คำบรรยายภาพ: ฝั่งซ้ายคือกับดัก — Order ก้อนเดียว (แดง) ที่ครัว ไรเดอร์ และบิลลิ่งต่างดึงเข้าหาตัว จนบวมเป็น god object ฝั่งขวาคือทางออก — ออเดอร์จริงใบเดียวถูกมองผ่าน4 Bounded Context (น้ำเงิน) ที่แต่ละอันมีคำว่า “order” ในภาษาของตัวเอง: Order ใน Ordering, Ticket ใน Kitchen, Assignment ใน Delivery, Charge ใน Payment เส้นที่แบ่งไม่ใช่เส้นเทคนิค แต่คือจุดที่ ubiquitous language เปลี่ยนชุด
4 context ที่ Event Storming เปิดออกมาแล้ว
หัวข้อที่มีชื่อว่า “4 context ที่ Event Storming เปิดออกมาแล้ว”ข่าวดีคือเราไม่ต้องนั่งเดาเส้นแบ่งพวกนี้บนกระดาษเปล่า มันโผล่ขึ้นมาให้เห็นตั้งแต่ Event Storming บทที่ 6 แล้ว — ตอนเรียง pivotal event บนไทม์ไลน์ (OrderPlaced, PaymentCaptured, FoodDelivered) เราสังเกตว่าตรงรอยต่อของ event เหล่านี้ คนที่คุยเปลี่ยนหน้า คำศัพท์เปลี่ยนชุด และกฎที่ต้องรักษาก็เปลี่ยนเรื่อง นั่นคือสัญญาณของขอบเขต context ที่โผล่ขึ้นเองจากผนัง ไม่ใช่จากสถาปนิกที่นั่งเทวดา
คอร์สนี้หยิบเบาะแสนั้นมาตัดเป็น4 bounded context ที่จะเดินคู่กันไปทั้งคอร์ส:
- Ordering (core) — ตะกร้าและการยืนยันคำสั่งซื้อ พูดภาษา
Cart,OrderLine,PlaceOrder - Kitchen (supporting) — คิวทำอาหาร พูดภาษา
Ticket,Station,MarkReady - Delivery (supporting) — การเคลื่อนของ พูดภาษา
Assignment,Rider,Route,Dropoff - Payment (generic) — เรื่องเงิน พูดภาษา
Charge,Refund,Captured
ในเวิร์กช็อป Event Storming งานครัว (FoodPrepared) ยังฝังรวมอยู่ในก๊วนการจัดส่ง คอร์สนี้แยก Kitchen ออกมาเป็น context ของตัวเองให้ชัด เพราะภาษาและกฎของ “การทำอาหารให้เสร็จ” ต่างจาก “การขนของจากต้นทางถึงปลายทาง” คนละเรื่องกัน การแยกแบบนี้ไม่ได้ผิดจากผนัง แต่คือการ ลากเส้น context ให้คมขึ้น เมื่อเราเตรียมจะลง code (บนผนังยังมี context เรื่อง Ratings/รีวิวหลังส่ง โผล่มาด้วย แต่คอร์สนี้ขอพักไว้นอกขอบเขต เพื่อโฟกัสสายหลักของออเดอร์ตั้งแต่กดสั่งจนส่งถึงมือ — เลยเหลือ4 context ที่เดินคู่กันไปทั้งคอร์ส)
ออเดอร์จริงใบเดียว หลาย model
หัวข้อที่มีชื่อว่า “ออเดอร์จริงใบเดียว หลาย model”ประเด็นหลักของบทนี้สรุปเป็นประโยคเดียว: ออเดอร์ในโลกจริงมีใบเดียว — ลูกค้ากดสั่งผัดกะเพราหนึ่งครั้ง — แต่ในซอฟต์แวร์มันสมควรมีหลาย model แต่ละ context มองออเดอร์ใบเดียวกันคนละมุม และถือเฉพาะข้อมูลกับกฎที่ตัวเองต้องรักษา ไม่มี context ไหนถือทุกอย่าง และไม่มี context ไหนต้องขออนุญาต context อื่นเพื่อรักษากฎของตัวเอง
นี่ไม่ใช่การทำสำเนาข้อมูลซ้ำๆ อย่างเปล่าประโยชน์ แต่คือการยอมรับความจริงว่าคำเดียวกันมีหลายความหมาย แล้วออกแบบให้แต่ละความหมายมีบ้านของตัวเอง เมื่อแต่ละ context เป็นเจ้าของ model ตัวเองเต็มตัว เราถึงจะแก้ครัวได้โดยไม่กระเทือนบิลลิ่ง และเปลี่ยนวิธีคิดเงินได้โดยไม่ต้องแตะคิวทำอาหาร
แต่พอแยกออกมาแล้ว คำถามถัดไปตามมาทันที: ถ้าแต่ละ context มี order ของตัวเอง แล้วมันจะรู้เรื่องกันได้ยังไง? ตอนลูกค้าจ่ายเงินเสร็จ Kitchen จะรู้ได้ยังไงว่าถึงคิวลงมือทำ? นั่นคือเรื่องของบทถัดๆ ไป — เราจะวาด context map เพื่อเห็นภาพว่าใครเชื่อมกับใคร กำหนดความสัมพันธ์ (partnership, customer-supplier, ACL) แล้วเชื่อมทุก context กลับเข้าด้วยกันอย่างเชื่อถือได้ด้วย integration event และ transactional outbox ก่อนจะแตก1 context ออกเป็น service จริงในตอนท้าย
การแยกให้ถูกก่อน คือเงื่อนไขที่ทำให้การเชื่อมกลับไม่กลายเป็นก้อนโคลนที่พันกันยุ่งอีกรอบ
เจาะลึกแนวคิดในบทนี้ต่อได้ที่คลังอ้างอิง DevIQ:
- Bounded Context — ขอบเขตที่ model หนึ่งชุดและภาษาหนึ่งชุดคงความหมายไว้ หัวใจของทั้งบท
- Subdomain — การแบ่งปัญหาธุรกิจเป็น core/supporting/generic ที่ต่างจาก bounded context
- Ubiquitous Language — ทำไมภาษากลางต้องผูกกับ context เดียว ไม่ใช่ทั้งองค์กร
- จาก Event Storming สู่ Bounded Context (Event Storming บทที่ 6) — จุดที่4 context นี้โผล่ขึ้นจากผนัง Post-it เป็นครั้งแรก
เช็กความเข้าใจ — บทที่ 1
ข้อ 1 / 3Subdomain กับ Bounded Context ต่างกันอย่างไร?