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

ทำไม​ต้อง​แยก context

ตลอด​คอร์ส​ที่​ผ่าน​มา​ใน​เว็บ​นี้ เรา​ปั้น domain ฟู้ด​เดลิ​เวอรี​ตัว​เดียวกัน​มา​ตลอด — เริ่ม​จาก Event Storming ที่​ค้น domain ทั้ง​ทีม​ด้วย​ผนัง Post-it ต่อ​ด้วย​การ​วาง​สถาปัตยกรรม​และ​ปั้น model Order ให้​ถือ​กฎ​ธุรกิจ​ไว้​ใน code แล้ว​พิสูจน์​ว่า​มัน​ทำงาน​ถูก​ด้วย test ทั้ง​พีระมิด ทุก​คอร์ส​ที่​ผ่าน​มา​มี Order แค่​ก้อน​เดียว และ​นั่น​ก็​เพียงพอ คอร์ส​นี้​เดิน​ต่อ​จาก​จุด​เดิม แต่​ถาม​คำถาม​ที่​ใหญ่​ขึ้น: เมื่อ Order ก้อน​เดียว​เริ่ม​แบก​ความหมาย​ของ​คน​หลาย​ฝ่าย​ไม่​ไหว เรา​จะ​แยก​มัน​อย่างไร?

📦 code ตัวอย่าง

code เต็ม​ของ​คอร์ส​นี้​อยู่​ที่ repo kaen-food-ordering (กำลัง​จัด​ทำ) — คอร์ส​นี้​ต่อยอด domain Order เดิม​ให้​กลาย​เป็น modular monolith 4 module บท​นี้​เป็น​ภาค​แนวคิด​ล้วนๆ ยัง​ไม่​แตะ​โครง code จริง (เริ่ม​ลง code ในบทถัดๆ ไป)

ตอน​ระบบ​ยัง​เล็ก การ​มี Order class เดียว​ที่​ทุก​ส่วน​ใช้​ร่วม​กัน​ดู​เป็น​เรื่อง​ดี — มี​ที่​เดียว​ให้​แก้ ไม่​ซ้ำซ้อน แต่​พอ feature โต Order ก้อน​นั้น​จะ​ค่อยๆ บวม​ขึ้น​ต่อ​เนื่อง เพราะ​แต่ละ​ฝ่าย​อยาก​ให้​มัน​ตอบ​โจทย์​ของ​ตัวเอง:

  • ครัว อยาก​รู้​ว่า​ต้อง​ทำ​เมนู​อะไร กี่​จาน อยู่​สเตชัน​ไหน ทำ​เสร็จ​หรือ​ยัง
  • ไร​เด​อร์ อยาก​รู้​ว่า​ไป​รับ​ที่ไหน ส่ง​ที่ไหน เส้นทาง​เป็น​ยังไง ถึง​หรือ​ยัง
  • บิล​ลิ่ง อยาก​รู้​ว่า​เก็บ​เงิน​สำเร็จ​ไหม ยอด​เท่าไร ถ้า​ยกเลิก​ต้อง​คืน​กี่​บาท

ทุก​ความ​อยาก​รู้​พวก​นี้​ไป​ลงเอย​ที่ Order ตัว​เดียวกัน กลาย​เป็น god object ที่​พอง​จน​ไม่มี​ใคร​เป็น​เจ้าของ​ความ​ถูกต้อง​ของ​มัน:

❌ version ดิบ: Order ก้อน​เดียว​ที่​พยายาม​เป็น​ทุก​อย่าง
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 หนึ่ง​ชุด​มี​ความหมาย​คง​เส้น​คง​วา นอก​ขอบเขต​นั้น​คำ​เดียวกัน​มี​สิทธิ์​หมาย​ถึง​คนละ​อย่าง

ก่อน​จะ​ลาก​เส้น​แบ่ง ต้อง​แยก​ให้​ออก​ก่อน​ว่า​มี​คำ​สอง​คำ​ที่​คน​มัก​ปน​กัน

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 เป็น​เส้น​ที่​เรา​เลือก​ลาก​เอง

เส้น​ที่​แบ่ง 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 เปลี่ยน​ชุด

ข่าวดี​คือ​เรา​ไม่​ต้อง​นั่ง​เดา​เส้น​แบ่ง​พวก​นี้​บน​กระดาษ​เปล่า มัน​โผล่​ขึ้น​มา​ให้​เห็น​ตั้งแต่ 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 แต่ละ context มองออเดอร์​ใบ​เดียวกัน​คนละ​มุม และ​ถือ​เฉพาะ​ข้อมูล​กับ​กฎ​ที่​ตัวเอง​ต้อง​รักษา ไม่มี context ไหน​ถือ​ทุก​อย่าง และ​ไม่มี context ไหน​ต้อง​ขอ​อนุญาต context อื่น​เพื่อ​รักษา​กฎ​ของ​ตัวเอง

นี่​ไม่ใช่​การ​ทำ​สำเนา​ข้อมูล​ซ้ำๆ อย่าง​เปล่า​ประโยชน์ แต่​คือ​การ​ยอมรับ​ความ​จริง​ว่า​คำ​เดียวกัน​มี​หลาย​ความหมาย แล้ว​ออกแบบ​ให้​แต่ละ​ความหมาย​มี​บ้าน​ของ​ตัวเอง เมื่อ​แต่ละ context เป็น​เจ้าของ model ตัวเอง​เต็ม​ตัว เรา​ถึง​จะ​แก้​ครัว​ได้​โดย​ไม่​กระเทือน​บิล​ลิ่ง และ​เปลี่ยน​วิธี​คิด​เงิน​ได้​โดย​ไม่​ต้อง​แตะ​คิว​ทำ​อาหาร

แต่​พอ​แยก​ออก​มา​แล้ว คำถาม​ถัด​ไป​ตาม​มา​ทันที: ถ้า​แต่ละ context มี order ของ​ตัวเอง แล้ว​มัน​จะ​รู้เรื่อง​กัน​ได้​ยังไง? ตอน​ลูกค้า​จ่าย​เงิน​เสร็จ Kitchen จะ​รู้​ได้​ยังไง​ว่า​ถึง​คิว​ลงมือ​ทำ? นั่น​คือ​เรื่อง​ของบทถัดๆ ไป — เรา​จะ​วาด context map เพื่อ​เห็น​ภาพ​ว่า​ใคร​เชื่อม​กับ​ใคร กำหนด​ความ​สัมพันธ์ (partnership, customer-supplier, ACL) แล้ว​เชื่อม​ทุก context กลับ​เข้า​ด้วย​กัน​อย่าง​เชื่อถือ​ได้​ด้วย integration event และ transactional outbox ก่อน​จะ​แตก1 context ออก​เป็น service จริง​ใน​ตอน​ท้าย

การ​แยก​ให้​ถูก​ก่อน คือ​เงื่อนไข​ที่​ทำให้การ​เชื่อม​กลับ​ไม่​กลาย​เป็น​ก้อน​โคลน​ที่​พัน​กัน​ยุ่ง​อีกรอบ


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

เจาะ​ลึก​แนวคิด​ใน​บท​นี้​ต่อ​ได้ที่​คลัง​อ้างอิง 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 / 3

Subdomain กับ Bounded Context ต่างกันอย่างไร?