Context Map — วาดความสัมพันธ์
บทที่แล้วเราลากเส้นแบ่งออเดอร์ก้อนเดียวออกเป็นสี่ Bounded ContextBounded Contextขอบเขตที่ ubiquitous language และ model หนึ่งชุดมีความหมายคงเส้นคงวา นอกขอบเขตนี้คำเดียวกันอาจหมายถึงคนละอย่าง เช่น 'order' ใน Ordering กับ 'ticket' ใน Kitchen คือมุมมองของสิ่งเดียวกันคนละ modelStrategic Design — Ordering (core), Kitchen (supporting), Delivery (supporting), Payment (generic) — แต่ละอันเป็นเจ้าของ model และภาษาของตัวเอง แต่พอแยกเสร็จก็โผล่คำถามใหม่ทันที: แล้วสี่ก้อนนี้จะคุยกันยังไง โดยไม่กลับไปพันกันเป็นก้อนโคลนเหมือนเดิม? เครื่องมือที่ตอบคำถามนี้คือ Context MapContext Mapแผนภาพที่วาดทุก Bounded Context และความสัมพันธ์ระหว่างกัน (Partnership, Customer-Supplier, ACL ฯลฯ) ให้เห็นภาพรวมว่าใครเชื่อมกับใคร และใครมีอำนาจกำหนด model มากกว่า (upstream/downstream)Strategic Design — แผนที่ที่วาดทุก context เป็นกล่อง แล้วลากเส้นระหว่างกล่องพร้อมติดป้ายว่าความสัมพันธ์คู่นั้น เป็นแบบไหน และ ใครมีอำนาจกำหนด model มากกว่า
code เต็มของคอร์สนี้อยู่ที่ repo kaen-food-ordering (กำลังจัดทำ) — คอร์สนี้ต่อยอด domain Order เดิมให้กลายเป็น modular monolith 4 module บทนี้ยังเน้นแนวคิดเชิงกลยุทธ์ (strategic design) แต่เริ่มแตะรูปร่าง code ของ integration event และ adapter ที่จะได้ลงมือจริงในบทถัดๆ ไป
ต้นน้ำ-ปลายน้ำ: ทิศของอำนาจก่อนจะพูดถึงแบบความสัมพันธ์
หัวข้อที่มีชื่อว่า “ต้นน้ำ-ปลายน้ำ: ทิศของอำนาจก่อนจะพูดถึงแบบความสัมพันธ์”ก่อนจะไล่ชื่อแบบความสัมพันธ์ ต้องเข้าใจแกนที่อยู่เบื้องหลังทุกแบบก่อน นั่นคือ ทิศของอำนาจ ในคู่ context สองอันที่พึ่งพากัน มักมีฝ่ายหนึ่งเป็น ต้นน้ำ (upstream) และอีกฝ่ายเป็น ปลายน้ำ (downstream)
- ต้นน้ำ (upstream) คือฝ่ายที่การเปลี่ยนแปลงของมัน ไหลลง ไปกระทบอีกฝ่าย ต้นน้ำเปลี่ยน contract เมื่อไร ปลายน้ำต้องปรับตาม
- ปลายน้ำ (downstream) คือฝ่ายที่ต้องรับผลจากต้นน้ำ ปลายน้ำเปลี่ยนภายในตัวเองได้อิสระ แต่เปลี่ยนแล้ว ไม่กระเทือน ต้นน้ำ
จุดที่คนพลาดบ่อยที่สุดคือคิดว่า “หัวลูกศรบนแผนที่ชี้ไปทางไหน ฝ่ายนั้นคือปลายน้ำ” — ไม่จริงเสมอไป ทิศของอำนาจกับทิศที่ code เรียกหากันเป็นคนละเรื่อง โดยเฉพาะเวลาเราต้องต่อกับของภายนอก เช่น Ordering เป็นคนกดเรียก payment gateway (code วิ่งจาก Ordering ออกไป) แต่ gateway ต่างหากที่เป็นต้นน้ำ เพราะมันเป็นคนกำหนด contract ที่เราแก้ไม่ได้ Ordering จึงเป็นปลายน้ำที่ต้องปรับตัวตาม เพราะเหตุนี้เราถึง เขียนป้าย upstream/downstream กำกับไว้ตรงๆ บนแผนที่ แทนที่จะให้คนอ่านเดาจากหัวลูกศร
แบบความสัมพันธ์เจ็ดแบบที่จะเล่าต่อไปนี้ ต่างกันที่ ปลายน้ำจัดการกับอำนาจของต้นน้ำอย่างไร — ยอมตามทั้งดุ้น, ต่อรองให้ต้นน้ำแคร์เรา, หรือสร้างกำแพงกันไม่ให้ model ต้นน้ำรั่วเข้ามา
เจ็ดแบบความสัมพันธ์บน context map
หัวข้อที่มีชื่อว่า “เจ็ดแบบความสัมพันธ์บน context map”แต่ละแบบมีคำอธิบายสั้นๆ และตัวอย่างจากฟู้ดเดลิเวอรีหนึ่งบรรทัด ให้เห็นภาพว่าเวลาจะใช้แบบไหน
-
PartnershipPartnershipความสัมพันธ์ที่2 context ต้องประสานแผนงานร่วมกันเพราะพึ่งพากันสองทาง หากฝ่ายใดล้มเหลวอีกฝ่ายก็ไปต่อไม่ได้ จึงต้องปล่อย feature ที่เกี่ยวข้องกันพร้อมกันเสมอStrategic Design — 2 context พึ่งพากันสองทาง ต้องวางแผนและปล่อยของ พร้อมกัน ถ้าฝ่ายหนึ่งพัง อีกฝ่ายก็ไปต่อไม่ได้ ไม่มีใครเป็นต้นน้ำชัดๆ ทั้งคู่ต้องประสานงานกันตลอด ตัวอย่าง: ถ้าจะทำ feature “ครัวกดว่าใกล้เสร็จ เพื่อให้ระบบเรียกไรเดอร์มารอล่วงหน้า” — Kitchen กับ Delivery ต้องล็อก spec ร่วมและปล่อยรุ่นพร้อมกัน มิฉะนั้น feature ครึ่งๆ กลางๆ จะพังทั้งคู่
-
Customer-SupplierCustomer-Supplierความสัมพันธ์ต้นน้ำ-ปลายน้ำที่ทีมต้นน้ำ (supplier) รับความต้องการของทีมปลายน้ำ (customer) เข้าไปวางแผนด้วย เช่น Ordering (supplier) ออกแบบ event ให้ Kitchen (customer) ใช้ได้จริงStrategic Design — ความสัมพันธ์ต้นน้ำ-ปลายน้ำที่ต้นน้ำ (supplier) ยอมรับความต้องการ ของปลายน้ำ (customer) เข้าไปในแผนงานของตัวเอง ปลายน้ำร้องขอได้ ต้นน้ำรับไปทำให้ — คนละเรื่องกับ conformist ที่ปลายน้ำร้องขออะไรไม่ได้เลย ตัวอย่าง: Ordering (supplier) ออกแบบ
OrderConfirmedIntegrationEventให้แนบข้อมูลพอที่ Kitchen (customer) จะเปิดตั๋วคิวได้จริง — Kitchen บอกได้ว่าอยากได้ field ไหน แล้ว Ordering รับไปใส่ให้ -
ConformistConformistทีมปลายน้ำยอม 'ตามใจ' model ของทีมต้นน้ำทั้งหมดโดยไม่แปลภาษา ใช้เมื่อทีมต้นน้ำไม่มีแรงจูงใจจะรองรับเรา แลกกับความเสี่ยงที่ model ภายนอกจะรั่วเข้ามาStrategic Design — ปลายน้ำ ยอมตาม model ของต้นน้ำทั้งดุ้น ไม่แปล ไม่ต่อรอง ใช้เมื่อต้นน้ำไม่มีแรงจูงใจจะแคร์เรา แลกความง่ายกับความเสี่ยงที่ model ของต้นน้ำจะไหลเข้ามาปนในบ้านเรา ตัวอย่าง: ถ้า Delivery ต้องดึงพิกัดจากผู้ให้บริการแผนที่ภายนอกรายใหญ่ที่ไม่มีทางแก้ API ให้เรา Delivery ก็ใช้ field ตามที่เขาให้มาเป๊ะๆ ยอมเป็น conformist เพราะสู้ไม่ไหว
-
Anti-Corruption LayerAnti-Corruption Layer (ACL)ชั้นแปลภาษา/model ที่กั้นระหว่าง context สองอัน ไม่ให้ model ของอีกฝ่ายรั่วเข้ามาปนเปื้อน เช่น PaymentGatewayAdapter ที่แปล Money/Order ของ Ordering เป็น DTO ของผู้ให้บริการชำระเงินภายนอกStrategic Design — ปลายน้ำสร้าง ชั้นแปล (adapter) กั้นระหว่างตัวเองกับต้นน้ำ ไม่ให้ type และ model ของอีกฝ่ายรั่วเข้ามาถึงใจกลาง domain ต่างจาก conformist ตรงที่เรายอมลงแรงสร้างกำแพงเพื่อ รักษา ความสะอาดของ model ตัวเองไว้ ตัวอย่าง: Ordering คุยกับ payment gateway ภายนอกผ่าน
PaymentGatewayAdapterที่แปลMoney.Thbเป็น DTO ของผู้ให้บริการ แล้วแปลผลกลับ — code domain ของ Ordering ไม่เคยเห็น type ของ gateway เลยสักตัว -
Open Host ServiceOpen Host Service (OHS)ทีมต้นน้ำเปิดโพรโทคอล/API ที่นิยามไว้ชัดเจนให้ผู้บริโภคหลายฝ่ายเชื่อมต่อได้โดยไม่ต้องเจรจาทีละคู่ เหมาะเมื่อมีหลาย context ต้องพึ่งพา service เดียวกันStrategic Design — ต้นน้ำเปิด API/โพรโทคอลสาธารณะที่นิยามไว้ชัด ให้ผู้บริโภค หลายฝ่าย ต่อได้โดยไม่ต้องเจรจาทีละคู่ เหมาะเมื่อมีหลาย context ต้องพึ่งพา service เดียวกัน ตัวอย่าง: ถ้าวันหน้าทั้ง Kitchen, Delivery และระบบวิเคราะห์ยอดขายอยากรู้ว่าออเดอร์ถูกยืนยันเมื่อไร Ordering ก็เปิดช่องทาง publish
OrderConfirmedแบบเดียวให้ทุกฝ่าย subscribe เป็นกลุ่ม แทนที่จะทำ integration แยกทีละคู่ -
Published LanguagePublished Languageภาษากลางที่เอกสารไว้ชัดเจนสำหรับแลกเปลี่ยนข้อมูลข้าม context เช่น สัญญา (contract) ของ integration event อย่าง FoodReadyIntegrationEvent มักมาคู่กับ Open Host ServiceStrategic Design — ภาษากลางที่ เอกสารไว้ชัด สำหรับแลกข้อมูลข้าม context คือ schema ของ Integration EventIntegration Eventevent ที่สื่อสาร 'ข้าม' Bounded Context เป็น contract แบบ serializable ที่มีแต่ id/ค่าพื้นฐาน (ไม่มี domain object) เช่น OrderConfirmedIntegrationEvent(Guid OrderId, DateTimeOffset ConfirmedAt) ต่างจาก Domain Event ที่อยู่ในโพรเซสเดียวArchitecture ที่ทุกฝ่ายผูกด้วย มักมาคู่กับ open host service (OHS เปิดประตู, published language คือรูปแบบข้อความที่ไหลผ่านประตูนั้น) ตัวอย่าง:
FoodReadyIntegrationEvent(Guid OrderId, Guid TicketId)คือสัญญาที่ Kitchen ประกาศออกมา ใครจะฟังก็ผูกกับ schema นี้ ไม่ใช่กับ typeTicketภายในของ Kitchen -
Separate WaysSeparate Waysตัดสินใจไม่เชื่อม2 context เข้าด้วยกันเลย เพราะต้นทุนการเชื่อมต่อไม่คุ้มกับประโยชน์ ปล่อยให้แต่ละฝ่ายแก้ปัญหาของตัวเองแบบง่าย ๆStrategic Design — ตัดสินใจ ไม่เชื่อม 2 context เข้าด้วยกันเลย เพราะต้นทุนการต่อไม่คุ้มกับประโยชน์ ปล่อยให้แต่ละฝ่ายแก้ปัญหาของตัวเองแบบตรงไปตรงมา ตัวอย่าง: Payment ไม่มีเหตุผลต้องรู้เรื่องสเตชันครัวหรือคิวทำอาหารเลย — 2 context นี้จึงไม่ลากเส้นถึงกัน แยกทางกันเดิน ไม่ต้องฝืนเชื่อม
วาดแผนที่ฟู้ดเดลิเวอรีจริง
หัวข้อที่มีชื่อว่า “วาดแผนที่ฟู้ดเดลิเวอรีจริง”พอมีคำศัพท์ครบแล้ว ก็ลงมือวาดของจริงได้ คอร์สนี้มี4 context แต่เส้นความสัมพันธ์ที่เป็นสายหลักของออเดอร์มีสามเส้น — จากกดสั่ง ไปครัว ไปถึงมือลูกค้า และแยกออกไปหาเรื่องเงิน
flowchart LR ORD["Ordering · core<br/>Order, PlaceOrder"] KIT["Kitchen · supporting<br/>Ticket, MarkReady"] DEL["Delivery · supporting<br/>Assignment, AssignRider"] PAY["Payment · generic<br/>gateway ภายนอก"] ORD -->|"Customer-Supplier<br/>Ordering = ต้นน้ำ · Kitchen = ปลายน้ำ<br/>OrderConfirmedIntegrationEvent"| KIT KIT -->|"Published Language<br/>Kitchen = ต้นน้ำ · Delivery = ปลายน้ำ<br/>FoodReadyIntegrationEvent"| DEL ORD -->|"Anti-Corruption Layer<br/>Payment = ต้นน้ำภายนอก · Ordering = ปลายน้ำ<br/>Ordering ห่อ gateway ด้วย adapter"| PAY classDef core fill:#c7d2fe,stroke:#3730a3,color:#1a1a1f; classDef supp fill:#bbf7d0,stroke:#166534,color:#1a1a1f; classDef gen fill:#fde68a,stroke:#92400e,color:#1a1a1f; class ORD core; class KIT,DEL supp; class PAY gen;
คำบรรยายภาพ: แผนที่ context ของฟู้ดเดลิเวอรี — Ordering (น้ำเงิน, core) เป็นศูนย์กลาง ยิงเหตุการณ์ไปยัง Kitchen (เขียว, supporting) แบบ customer-supplier แล้ว Kitchen ยิงต่อไป Delivery (เขียว) ด้วย published language ส่วนเส้นลงล่างไป Payment (เหลือง, generic) เป็น anti-corruption layer ป้ายบนเส้นบอกทั้ง แบบความสัมพันธ์ และ ใครต้นน้ำใครปลายน้ำ — สังเกตว่าเส้นไป Payment หัวลูกศรชี้จาก Ordering ออกไป (Ordering เป็นคนเรียก) แต่ป้ายบอกว่า Payment ต่างหากคือต้นน้ำ นี่คือจุดที่ทิศลูกศรกับทิศอำนาจสวนกัน
เส้นที่ 1 — Ordering → Kitchen: Customer-Supplier
หัวข้อที่มีชื่อว่า “เส้นที่ 1 — Ordering → Kitchen: Customer-Supplier”เมื่อลูกค้าจ่ายเงินและออเดอร์ถูกยืนยัน Ordering จะปล่อย OrderConfirmedIntegrationEvent(Guid OrderId, DateTimeOffset ConfirmedAt) ออกมา Kitchen รับไปเปิด Ticket ใหม่เข้าคิวทำอาหาร ความสัมพันธ์นี้เป็น customer-supplier เพราะ Ordering (ต้นน้ำ/supplier) กับ Kitchen (ปลายน้ำ/customer) เป็นทีมในบริษัทเดียวกันที่คุยกันได้ — Kitchen ร้องขอได้ว่า event ต้องแนบข้อมูลอะไรบ้างจึงจะเปิดตั๋วได้ครบ แล้ว Ordering รับความต้องการนั้นเข้าไปออกแบบ contract ให้ ไม่ใช่โยน event หน้าตาอะไรก็ได้มาให้ Kitchen เดาเอง
ทำไมไม่ใช่ conformist? เพราะ conformist คือปลายน้ำที่ ร้องขออะไรไม่ได้เลย ต้องกลืน model ต้นน้ำทั้งดุ้น แต่ที่นี่ Kitchen มีปากมีเสียง Ordering ฟัง — จึงเป็น customer-supplier ที่เป็นธรรมกว่า
เส้นที่ 2 — Kitchen → Delivery: Published Language
หัวข้อที่มีชื่อว่า “เส้นที่ 2 — Kitchen → Delivery: Published Language”พออาหารทำเสร็จ Kitchen ปล่อย FoodReadyIntegrationEvent(Guid OrderId, Guid TicketId) Delivery รับไปสร้าง Assignment แล้วจับคู่ไรเดอร์ ความสัมพันธ์นี้เป็น published language เพราะสิ่งที่ผูกสองฝ่ายไว้ไม่ใช่การคุยกันเป็นรายๆ แต่คือ schema ของ event ที่ประกาศไว้ชัด — Kitchen เผยแพร่รูปแบบข้อความนี้ออกมาเป็นสัญญาสาธารณะ ใครจะฟังก็ผูกกับ schema ไม่ใช่ผูกกับ type Ticket ภายในของ Kitchen ถ้าพรุ่งนี้มีระบบแจ้งเตือนลูกค้าอยากฟัง “อาหารพร้อมแล้ว” ด้วย ก็แค่ subscribe event เดิม ไม่ต้องแก้อะไรที่ Kitchen เลย นี่คือข้อดีของการสื่อสารผ่านภาษาที่เผยแพร่ไว้แทนการผูกตรงกับ model ภายใน
จุดสำคัญที่ต้องย้ำ: FoodReadyIntegrationEvent เป็น integration event ไม่ใช่ Domain EventDomain Eventสิ่งที่เกิดขึ้นแล้วภายใน context เดียว ทำงาน in-process และอ้างอิง domain object ได้ตรง ๆ เช่น OrderConfirmed ภายใน Ordering — เมื่อจะส่งข้ามขอบเขต context ต้องแปลงเป็น Integration Event ก่อนเสมอ ห้ามส่ง domain event ข้ามไปตรง ๆTactical Design — มันข้ามขอบเขต context จึงพก แค่ id กับค่าพื้นฐาน (Guid OrderId, Guid TicketId) ห้ามแนบ Ticket ทั้งก้อนซึ่งเป็น domain object ของ Kitchen ออกไป เพราะนั่นเท่ากับรื้อกำแพงที่เราเพิ่งสร้าง domain event อย่าง TicketReady อยู่ในบ้าน Kitchen เท่านั้น พอจะข้ามออกไปหา Delivery ต้องแปลงเป็น integration event ก่อนเสมอ
เส้นที่ 3 — Ordering → Payment: Anti-Corruption Layer
หัวข้อที่มีชื่อว่า “เส้นที่ 3 — Ordering → Payment: Anti-Corruption Layer”Payment เป็น subdomain แบบ generic — เราไม่ปั้น payment gateway เอง แต่ไปต่อกับผู้ให้บริการภายนอก model ของผู้ให้บริการรายนั้นเราแก้ไม่ได้ มันจึงเป็น ต้นน้ำ เต็มตัว ถ้าปล่อยให้ DTO ของ gateway ไหลเข้ามาถึงใจกลาง Ordering วันหนึ่งที่ผู้ให้บริการเปลี่ยน field หรือเราย้ายไปเจ้าอื่น domain ทั้งก้อนจะพังตาม เพราะเหตุนี้ Ordering (ปลายน้ำ) จึงต้องสร้าง anti-corruption layer — พูดกับ gateway ผ่าน port ของตัวเองแล้วให้ adapter เป็นคนแปล
เริ่มจาก version ที่ยังไม่มีกำแพง:
public class ConfirmOrderHandler{ private readonly AcmePayClient _acme; // type ของผู้ให้บริการภายนอกโผล่ใน domain
public async Task Handle(Order order, CancellationToken ct) { // รูปร่างคำขอของ AcmePay ผูกติดกับตรรกะของ Ordering โดยตรง var req = new AcmeChargeRequest { reference = order.Id.Value.ToString(), amountMinor = (long)(order.Total.Amount * 100), // ภาษาของ AcmePay ไม่ใช่ของเรา currencyCode = "THB", }; await _acme.ChargeAsync(req, ct); // ย้ายเจ้าเมื่อไร หรือ AcmePay เปลี่ยน schema เมื่อไร ตรงนี้พังทันที }}แล้วห่อด้วย ACL — Ordering เห็นแค่ port ที่พูดภาษาของตัวเอง ส่วน adapter ที่แปลไป-กลับอยู่ นอก domain (port IPaymentGateway version คอร์สนี้ต่อยอดของไตรภาคที่เป็นแบบ fire-and-forget ให้คืน PaymentResult กลับมา เพื่อให้ ACL แปลคำตอบของ gateway กลับเข้า domain ได้ — บท 5 เจาะรายละเอียด):
// port ที่ Ordering เป็นเจ้าของ พูดด้วย Money.Thb / OrderId ของ Ordering เองpublic interface IPaymentGateway{ Task<PaymentResult> ChargeAsync(OrderId orderId, Money amount, CancellationToken ct); Task<PaymentResult> RefundAsync(OrderId orderId, Money amount, CancellationToken ct);}
public record PaymentResult(bool Captured, string GatewayReference);
// adapter = ตัว ACL: แปล Money.Thb <-> DTO ของ AcmePay อยู่ที่ชั้น infrastructurepublic sealed class PaymentGatewayAdapter : IPaymentGateway{ private readonly AcmePayClient _acme;
public PaymentGatewayAdapter(AcmePayClient acme) => _acme = acme;
public async Task<PaymentResult> ChargeAsync(OrderId orderId, Money amount, CancellationToken ct) { var req = new AcmeChargeRequest { reference = orderId.Value.ToString(), amountMinor = (long)(amount.Amount * 100), currencyCode = "THB" }; var res = await _acme.ChargeAsync(req, ct); // ภาษาของ AcmePay ตายอยู่ในนี้ return new PaymentResult(res.status == "SUCCESS", res.txnId); }
// RefundAsync แปลทำนองเดียวกัน}ตอนนี้ถ้าย้ายไปผู้ให้บริการเจ้าอื่น เราเขียน adapter ตัวใหม่ที่ implement IPaymentGateway เหมือนเดิม โดยไม่แตะ code domain ของ Ordering แม้แต่บรรทัดเดียว — นั่นคือคุณค่าของ ACL: มันดูดแรงกระแทกจากการเปลี่ยนแปลงของต้นน้ำภายนอกไว้ที่ชั้นเดียว
รูปร่างของ integration event และ bus ที่วิ่งข้ามขอบเขตในระบบนี้หน้าตาแบบนี้ — ทุกตัวมีแต่ id/ค่าพื้นฐาน ไม่มี domain object แม้แต่ตัวเดียว:
public interface IIntegrationEvent { }
// Ordering ประกาศ (customer-supplier: Ordering = ต้นน้ำ)public record OrderConfirmedIntegrationEvent(Guid OrderId, DateTimeOffset ConfirmedAt) : IIntegrationEvent;
// Kitchen ประกาศ (published language ให้ Delivery และคนอื่นฟัง)public record FoodReadyIntegrationEvent(Guid OrderId, Guid TicketId) : IIntegrationEvent;
// Delivery ประกาศ ต่อสายให้ปลายทางฟังpublic record OrderDeliveredIntegrationEvent(Guid OrderId, Guid AssignmentId) : IIntegrationEvent;
public interface IIntegrationEventBus{ Task PublishAsync(IIntegrationEvent e, CancellationToken ct);}แล้วอีกสี่แบบที่ไม่ได้อยู่บนแผนที่ล่ะ?
หัวข้อที่มีชื่อว่า “แล้วอีกสี่แบบที่ไม่ได้อยู่บนแผนที่ล่ะ?”เราใช้จริงแค่สามแบบจากเจ็ด — customer-supplier, published language, ACL — แต่ที่ต้องรู้จักทั้งเจ็ดเพราะ การเลือกไม่ใช้ก็เป็นการตัดสินใจ ที่ต้องอธิบายได้:
- partnership ไม่ได้ใช้เพราะเราจงใจออกแบบให้แต่ละ context ปล่อยของได้อิสระ ไม่ต้องล็อก spec ปล่อยพร้อมกัน การสื่อสารผ่าน integration event ทำให้ต่างฝ่ายต่างเดินได้เอง
- open host service ยังไม่จำเป็นตอนนี้เพราะแต่ละ event มีผู้ฟังหลักฝ่ายเดียว แต่ถ้าผู้ฟังเพิ่มขึ้นหลายฝ่าย Ordering ควรยกระดับช่องทาง
OrderConfirmedขึ้นเป็น OHS เต็มตัว - conformist เราเลี่ยงตรงจุด Payment โดยยอมลงแรงทำ ACL แทน — เพราะ model เรื่องเงินสำคัญเกินกว่าจะปล่อยให้ภาษาของ gateway ภายนอกไหลเข้ามาปน
- separate ways คือความสัมพันธ์ระหว่าง Payment กับ Kitchen/Delivery ที่เราจงใจ ไม่ลากเส้น เพราะไม่มีเหตุผลทางธุรกิจให้เชื่อม
พอทุกเส้นบนแผนที่มีเหตุผลกำกับ — ทั้งเส้นที่ลากและเส้นที่จงใจไม่ลาก — context map ก็กลายเป็นเอกสารที่บอกได้ว่าระบบนี้ถูกออกแบบมาให้เปลี่ยนตรงไหนได้โดยไม่กระเทือนที่อื่น
สรุปบทนี้
หัวข้อที่มีชื่อว่า “สรุปบทนี้”context map ไม่ใช่แค่กล่องกับลูกศรสวยๆ แต่คือแผนที่อำนาจ — มันบอกว่าใครกำหนด model ให้ใคร (upstream/downstream) และปลายน้ำแต่ละคู่เลือกรับมือกับอำนาจนั้นด้วยวิธีไหน สำหรับฟู้ดเดลิเวอรีของเรา สายหลักคือ Ordering ป้อน Kitchen แบบ customer-supplier, Kitchen ป้อน Delivery ด้วย published language, และ Ordering กันตัวเองจาก payment gateway ภายนอกด้วย ACL
บทถัดไปเราจะซูมเข้าไปที่ตัว event เหล่านี้ให้ลึกขึ้น — ความต่างระหว่าง domain event กับ integration event, ทำไมส่ง domain object ข้ามขอบเขตไม่ได้, และจะแปลงจากอันหนึ่งเป็นอีกอันตรงไหนของ code เพราะเส้นบนแผนที่ที่เราเพิ่งวาด จะกลายเป็นข้อความจริงที่วิ่งข้ามขอบเขตในบทต่อๆ ไป
เจาะลึกแนวคิดในบทนี้ต่อได้ที่คลังอ้างอิง DevIQ:
- Context Map — แผนภาพความสัมพันธ์ระหว่าง bounded context และแบบความสัมพันธ์ทั้งหมด หัวใจของทั้งบท
- Anti-Corruption Layer — ชั้นแปลที่กัน model ของต้นน้ำไม่ให้รั่วเข้ามา เหมือนที่ Ordering ใช้กับ payment gateway
- Bounded Context — ขอบเขตที่ model และภาษาหนึ่งชุดคงความหมายไว้ สิ่งที่เส้นบนแผนที่เชื่อมถึงกัน
เช็กความเข้าใจ — บทที่ 2
ข้อ 1 / 3Kitchen ประกาศ schema ของ FoodReadyIntegrationEvent(Guid OrderId, Guid TicketId) ออกมาเป็นสัญญาสาธารณะ ใครจะฟังก็ผูกกับ schema นี้ ไม่ใช่กับ type Ticket ภายในของ Kitchen — นี่คือความสัมพันธ์แบบใด?