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

ออกแบบ User Interface ของ application

ลูกค้า​ส่วน​ใหญ่​สนใจ​แค่ หน้า​จอ ของ application เรา ถ้า​มัน​สวย ใช้​ง่าย และ​พา​เขา​ไป​ถึง​สิ่ง​ที่​ต้องการ​ได้ — เช่น พบ package ทัวร์​ใน​ฝัน — เขา​ก็​ยินดี​จ่าย​เงิน แต่​การ​ทำ UI ให้​ดี​นั้น​ไม่ใช่​แค่​เรื่อง​ของดีไซเนอร์​ที่​รังสรรค์​ภาพสวยๆ เพราะ​เบื้องหลัง​ยัง​มี​โจทย์​วิศวกรรม​ที่​หนักหน่วง​ซ่อน​อยู่ ทั้ง​เรื่อง performance, scalability และ loose coupling ของ Bounded ContextBounded Contextขอบเขต​ที่​ชัดเจน (มัก​เป็น​ระบบ​ย่อย​หรือ​ทีม​หนึ่ง) ที่ model หนึ่ง​ใช้ได้​และ​มี​ความหมาย​แน่นอน ภายใน​ขอบเขต​นี้ Ubiquitous Language สอดคล้อง​กัน 100%Strategic Design ที่​ทำงาน​อยู่​หลัง​ฉาก

บท​นี้​เรา​จะ​ตอบ​คำถาม​ที่​ฟัง​ดู​ง่าย​แต่จริงๆ แล้ว​ยาก: เมื่อ​ข้อมูล​กระจัดกระจาย​อยู่​หลาย Bounded Context หน้า UI เดียว​จะ “ประกอบ​ร่าง” ข้อมูล​ทั้งหมด​เข้า​ด้วย​กัน​ได้​อย่างไร โดย​ไม่​ทำให้​ระบบ​กลับ​มา​ผูก​กัน​แน่น (coupling) อีก​ครั้ง


ใน app e-commerce หน้า​เดียว คุณ​อาจ​อยาก​แสดง​รายการ​สินค้า​ใน catalog ราคา ตัว​เลือก​การ​จัด​ส่ง โปรโมชั่นพิเศษ และอื่นๆ พร้อม​กัน จาก Part II เรา​รู้​แล้ว​ว่า​ใน​ระบบ​แบบ event-driven ข้อมูล​พวก​นี้​ถูก​เก็บ​กระจาย​อยู่​ใน​หลาย Bounded Context ที่ sync กัน​แบบ Eventual ConsistencyEventual Consistencyความ​ถูกต้อง​ที่ “ตาม​มา​ทีหลัง” ไม่ใช่​ทันที ใช้​กับ​การ​อัปเดต​ข้าม Aggregate ผ่าน Domain Event แทน​การ​บังคับ​ให้​ตรง​กัน​ใน​ทันที​ภายใน transaction เดียวTactical Design

ที่​สำคัญ​กว่า​นั้น ระบบ​เหล่า​นี้​เป็น​แบบ share-nothing — เว็บ app จะ​ยิง query ตรง​เข้า​ฐาน​ข้อมูล​ของ Bounded Context อื่น​ไม่​ได้​เด็ดขาด เพราะ​นั่น​คือ​การ​เพิ่ม coupling กลับ​เข้า​มา การ​ที่​แต่ละ context เป็น​เจ้าของ​ฐาน​ข้อมูล​ของ​ตัวเอง​อย่าง​เด็ดขาด​ต่างหาก​คือ​สิ่ง​ที่​ทำให้​มัน​เปลี่ยนแปลง​ได้​อย่าง​อิสระ

กับดัก​ที่​ทำลาย​ขอบเขต

อย่า​ให้​เว็บ app ไป​อ่าน​ฐาน​ข้อมูล​ของ Bounded Context อื่น​โดยตรง​เพียง​เพราะ​มัน​สะดวก เพราะ​นั่น​เท่ากับ​ทุบ​กำแพง​ที่​เรา​อุตส่าห์​สร้าง​ขึ้น เมื่อใด​ที่ context หนึ่ง​เปลี่ยน schema หน้า​เว็บ​ก็​จะ​พัง​ทันที — แล้ว​เรา​จะ​ถลำ​กลับ​สู่ Big Ball of MudBig Ball of Mudระบบ​ที่​ปนเป​กัน​ยุ่งเหยิง​ไม่มี​ขอบเขต​ชัด ให้​ลาก​เส้น​ล้อม​ไว้​แล้ว​ยอมรับ​สภาพ ไม่​พยายาม​ทำ model ละเอียด​ใน​นั้น และ​ระวัง​ไม่​ให้​ลาม​ไป context อื่นStrategic Design โดย​ไม่รู้ตัว

แล้ว​จะ​แก้​อย่างไร? มี​ทาง​เลือก​หลาย​แบบ แต่ละ​แบบ​มี trade-off ของ​มัน เรา​จะ​แบ่ง​การ​ตัดสิน​ใจ​ออก​เป็น​สาม​แกน​หลัก แล้ว​ปิด​ท้าย​ด้วย​ตัวอย่าง​จริง​สอง​ชุด


คำถาม​แรก​ใน​การ​ออกแบบ UI คือ “ใน​เชิง​ตรรกะ​แล้ว ใคร​เป็น​เจ้าของ​หน้า​นี้?” หน้า​จอ​อาจ​อยู่​ภายใน Bounded Context เดียว​และ​เป็น​ของ​ทีม​นั้น หรือ​อาจ​ดึง​ข้อมูล​จาก​หลาย context แต่​ไม่​ได้​เป็น​ของ​ใคร​เลย​ก็ได้

UI แบบ autonomous เป็น​ของ business component เดียว ไม่​ต้อง​ไป​ดึง​ข้อมูล​จาก context อื่น​ตอน render เลย แต่​นั่น​ก็​แปล​ว่า context นี้​ต้อง เก็บ​ข้อมูล​ทุก​อย่าง​ที่​จะ​แสดง​บน​หน้า​จอ​ไว้​กับ​ตัวเอง วิธี​ทำ​คือ subscribe รับ Domain EventDomain Eventสิ่ง​ที่ “เกิด​ขึ้น​แล้ว” ใน domain ซึ่ง​ส่วน​อื่น​สนใจ แทน​ด้วย​อ็อบเจ็กต์ที่​เปลี่ยนแปลง​ไม่​ได้ ตั้ง​ชื่อ​เป็น​อดีต เช่น CargoWasRouted ใช้​สื่อสาร​ข้าม AggregateTactical Design จาก context อื่น แล้ว​เก็บ​สำเนา (copy) ของ​ข้อมูล​ที่​ต้อง​ใช้​ไว้​ใน local

รูปที่ 23-1: UI แบบ autonomous — Content Enhancement business component เก็บข้อมูลทุกอย่างที่หน้าจอต้องใช้ไว้ใน DB ของตัวเอง โดย subscribe รับ Sale Completed event จาก Sales context มาเก็บไว้ล่วงหน้า

ใน​รูป​ที่ 23-1 ทีม catalog มีหน้า​เครื่องมือ​สำหรับ​ปรับ​แต่ง​คอนเทนต์​ของ​สินค้า พวก​เขา​อยาก​รู้​ว่า​สินค้า​ตัว​ไหน​ขาย​ดี​แค่​ไหน เพื่อ​จะ​ได้​ทุ่ม​แรง​ทำ​คอนเทนต์​ให้​ถูก​ตัว ข้อมูล​ยอด​ขาย​นี้​ได้​มา​จาก​การ subscribe รับ Sale Completed event จาก Sales context แล้ว​เก็บ​ไว้​ใน Content Enhancement DB — พร้อม​แสดง​ทันที​โดย​ไม่​ต้อง​ไป​ถาม​ใคร

ข้อ​ควร​ระวัง​คือ Eventual ConsistencyEventual Consistencyความ​ถูกต้อง​ที่ “ตาม​มา​ทีหลัง” ไม่ใช่​ทันที ใช้​กับ​การ​อัปเดต​ข้าม Aggregate ผ่าน Domain Event แทน​การ​บังคับ​ให้​ตรง​กัน​ใน​ทันที​ภายใน transaction เดียวTactical Design: ข้อมูล​ที่​แสดง​อาจ​ไม่ใช่​ค่า​ล่าสุด​อย่าง​แม่นยำ สำหรับ​กรณี​นี้​ไม่​เป็นไร — ตัวเลข​ยอด​ขาย​ที่​ช้า​ไป​ไม่​กี่​นาที กี่​ชั่วโมง หรือ​แม้แต่​กี่​วัน​ก็​ยัง​ใช้​ตัดสิน​ใจ​ได้ แต่​ถ้า ความ​สด​ใหม่​ของ​ข้อมูล​สำคัญ​มาก autonomous อาจ​ไม่ใช่​ทาง​เลือก​ที่​ดี

เมื่อ​คุณ​ต้องการ snapshot ล่า​สุดจริงๆ จาก​หลาย context ใน​หน้า​เดียว app ต้อง ร้องขอ​ข้อมูล​แต่ละ​ชิ้น​ตรง​ไป​ยัง context ที่​เป็น​เจ้าของ​ข้อมูล​นั้น (authority) ทุก​ครั้ง​ที่​โหลด​หน้า

รูปที่ 23-2: UI แบบ authoritative — หน้าเว็บ e-commerce ยิง HTTP GET ไปยัง Sales, Catalog และ Marketing context ทุกครั้งที่ประกอบหน้า ข้อมูลจึงสดเสมอ และหน้าเว็บนี้ไม่ได้เป็นของ context ใดเลย

จุด​สำคัญ​ใน​รูป​ที่ 23-2: หน้า​เว็บ​นี้ ไม่​ได้​เป็น​ของ Bounded Context ใด​เลย หลาย​บริษัท​จึง​มี​ทีม​เว็บ​โดย​เฉพาะ​ที่​ไม่​ได้​เป็น​เจ้าของ context ไหน แต่​รับผิดชอบ​เว็บไซต์​ทั้งหมด หรือ​ถ้า​จะ​ให้​ทีม​ที่​เป็น​เจ้าของ context หนึ่ง​มา​ดูแล​หน้า​นี้​ก็ได้ ขอ​แค่​จำ​ไว้​ว่า ใน​เชิง​แนวคิด หน้า​นี้​ไม่ใช่​ส่วน​หนึ่ง​ของ context นั้น

เลือก autonomous หรือ authoritative อย่างไร

ดู​ที่ ความ​เป็น​เจ้าของ​ของ​ทีม และ ความ​สด​ใหม่​ของ​ข้อมูล: ถ้า​เป็น​เครื่องมือ​ภายใน​ของ​แผนก​ใด​แผนก​หนึ่ง​และ​ข้อมูล​ช้า​ได้​บ้าง → autonomous (เก็บ local) คุ้ม​กว่า แต่​ถ้า​เป็น​ส่วน​หนึ่ง​ของ​เว็บ​สาธารณะ​ที่​ต้องการ​ข้อมูล​ล่าสุด​เสมอ → authoritative (ถาม​ต้นทาง) เหมาะ​กว่า อย่า​ลงทุน​เก็บ​ข้อมูล​ซ้ำซ้อน​มหาศาล​เพียง​เพื่อ use case เล็กๆ


เมื่อ​ตัดสิน​ใจ​แล้ว​ว่า​จะ​ดึง​ข้อมูล​จาก​หลาย context สิ่ง​ต่อ​มา​คือ แต่ละ context จะ​ส่ง​อะไร​กลับ​มา?

ทาง​เลือก A — HTML API: แต่ละ context ส่ง “ชิ้น​ส่วน HTML” กลับ​มา แล้ว​นำ​มา​แปะ​ลง​หน้า​เว็บได้ตรงๆ วิธี​นี้​มอบ​อำนาจ​ให้​แต่ละ context ควบคุม หน้าตา​และ​พฤติกรรม ของ​พื้นที่​ที่​ตัวเอง​รับผิดชอบ​บน​หน้า​จอ

ทาง​เลือก B — Data API: แต่ละ context ส่ง​แค่ “ข้อมูล​ดิบ” (มัก​เป็น JSON หรือ XML) กลับ​มา แล้ว​เว็บ app จัดการ​เรื่อง​การ​แสดง​ผล​ทั้งหมด​ไว้​ที่​เดียว

flowchart LR
  subgraph A["ทางเลือก A — HTML API"]
    BC1[Bounded Context] -->|HTML| W1[Web Page]
    BC2[Bounded Context] -->|HTML| W1
    BC3[Bounded Context] -->|HTML| W1
  end
  subgraph B["ทางเลือก B — Data API"]
    BC4[Bounded Context] -->|JSON| W2[Web Page<br/>จัดการ presentation เอง]
    BC5[Bounded Context] -->|JSON| W2
    BC6[Bounded Context] -->|JSON| W2
  end

จาก​รายงาน​ประสบการณ์​จริง​ส่วน​ใหญ่ ทาง​เลือก B (JSON API) เป็น​ที่​นิยม​มากกว่า​อย่าง​ชัดเจน แต่​ทั้ง​สอง​แบบ​ใช้งาน​ได้​จริง ข้อ​พิจารณา​สำคัญ​ข้อ​หนึ่ง​คือ คุณ​เปิด API ให้​ภายนอก​ใช้​ด้วย​หรือ​ไม่ — ถ้า​ใช่ การ “dogfood” API ของ​ตัวเอง (ใช้ API เดียว​กับ​ที่​ลูกค้า​ใช้) มี​ข้อดี​หลาย​อย่าง (ดู​บท​ที่ 13 เรื่อง Integrating Via HTTP)

trade-off ของ​สอง​แบบ

HTML มอบ​อำนาจ​การ​แสดง​ผล​ให้​แต่ละ context (เปลี่ยน​หน้าตา​ส่วน​ของ​ตัวเอง​ได้​โดย​ไม่​กระทบ​ใคร) แต่​ทำให้ presentation logic กระจัดกระจาย — ส่วน Data/JSON รวบ presentation มา​ไว้​ที่​เดียว ดูแล​ง่าย​และ​สม่ำเสมอ แต่ context จะ​ไม่มี​สิทธิ์​กำหนด​หน้าตา​ส่วน​ของ​ตัวเอง ไม่มี​แบบ​ไหน “ดี​กว่า” โดย​สมบูรณ์ มัน​คือ​การ​เลือก​ตาม trade-off


เมื่อ​ต้อง​ดึง​เนื้อหา (ไม่​ว่า​จะ HTML หรือ data) จาก​หลาย context มี สอง​ที่ ที่​จะ​ทำการ “ประกอบ​ร่าง” ได้:

  • ฝั่ง client: ยิง​แต่ละ​คำขอ​เป็น AJAX request จาก​หน้า​เว็บ​โดยตรง — ลด​ความ​ซับซ้อน​และ​จุด​ที่​อาจ​ล้มเหลว​ของ server ตัวกลาง แต่​แลก​มา​ด้วย JavaScript ฝั่ง client ที่​ซับซ้อน​ขึ้น (ปัญหา​นี้​เบา​ลง​มาก​ใน​ยุค Single Page Application หรือ SPA)
  • ฝั่ง server: ให้ web application ตัวกลาง​วิ่ง​ไป​ดึง​จาก​ทุก context มา​ประกอบ​เป็น​เนื้อหา​เดียว​ก่อน​ส่ง​ให้ browser — ตัด dependency กับ JavaScript และ​ข้อ​จำกัด​ด้าน performance ของ​การ​รัน​ใน browser ออก​ไป
flowchart TB
  subgraph S["ประกอบฝั่ง Server"]
    direction LR
    WA[Web Application] --> bcA[Bounded Context]
    WA --> bcB[Bounded Context]
    WA --> bcC[Bounded Context]
    WA --> WP1[Web Page<br/>รับ Aggregated Content]
  end
  subgraph C["ประกอบฝั่ง Client"]
    direction LR
    WP2[Web Page] -.AJAX.-> bcX[Bounded Context]
    WP2 -.AJAX.-> bcY[Bounded Context]
    WP2 -.AJAX.-> bcZ[Bounded Context]
  end

คำ​แนะนำ​ทั่วไป​มัก​เอนเอียง​ไป​ทาง client แต่​ทั้ง​สอง​แบบ​ก็​ใช้​กัน​แพร่หลาย ต่อ​ไป​เรา​จะ​ดู​ตัวอย่าง​จริง​ทั้ง​สอง​แนวทาง


ตัวอย่าง​ที่ 1: ประกอบ​ฝั่ง Server ด้วย HTML API (context ไม่​กระจาย)

หัวข้อ​ที่​มีชื่อ​ว่า “ตัวอย่าง​ที่ 1: ประกอบ​ฝั่ง Server ด้วย HTML API (context ไม่​กระจาย)”

แม้ Bounded Context ทั้งหมด​จะ​อยู่​ใน solution เดียว​และ​รัน​เป็น app เดียว การ​ประกอบ UI ก็​ยัง​มี​ประโยชน์​ใน​แง่ แบ่ง​ความ​รับผิดชอบ​ด้าน​การ​แสดง​ผล​ให้​แต่ละ context เมื่อ context หนึ่ง​อยาก​แก้​ส่วน​ของ​ตัวเอง​บน​หน้า​จอ การ​เปลี่ยนแปลง​จะ​ถูก​จำกัด​อยู่​ใน context นั้น ไม่​กวน​คน​อื่น — นี่​คือ​เจตนา​เดียว​กับ​หลัก Single Responsibility Principle (SRP)

รูปที่ 23-7: design ของตัวอย่างที่ 1 — หน้าตะกร้าสินค้าเรียก RenderAction() เพื่อดึงชิ้นส่วน HTML จาก Pricing, Catalog และ Shipping context ที่อยู่ใน Visual Studio solution เดียวกัน

ตัวอย่าง​ต้นฉบับ​ใช้ RenderAction() ของ ASP.NET MVC หน้า​เว็บ​ทำตัว​เป็น​แค่ template เปล่าๆ แล้ว​ดึง​เนื้อหา​หลัก​มา​ด้วย​การ render HTML ที่​แต่ละ context ส่ง​มา แนวคิด​หลัก​คือ: controller ของ​แต่ละ context จะ​ดึง​ข้อมูล​ที่​ตัวเอง​ต้องการ​จาก context ของ​ตัวเอง​เท่านั้น

หน้า composite UI (Razor) — เรียก HTML จาก​แต่ละ context
@foreach (var pid in productIdsInBasket)
{
<div class="basketItem">
@* ดึง HTML ของรายการสินค้า จาก Catalog context *@
@{ Html.RenderAction("ItemInBasket", "CatalogBoundedContext",
new { productId = pid }); }
@* ดึง HTML ของราคา จาก Pricing context *@
@{ Html.RenderAction("Price", "PricingBoundedContext",
new { productId = pid }); }
</div>
}
@* ดึง HTML ของตัวเลือกการจัดส่ง จาก Shipping context *@
@{ Html.RenderAction("DeliveryOptions", "ShippingBoundedContext"); }

หัวใจ​ใน​เชิง​แนวคิด​อยู่​ที่ controller ของ​แต่ละ context: มัน​ต้อง​ดึง​ข้อมูล​ทุก​อย่าง​ที่​ต้อง​ใช้ จาก context ของ​ตัวเอง​เท่านั้น ถ้า controller ของ Catalog ไป​เรียก method ของ context อื่น เรา​ก็​จะ​ได้ coupling ข้าม context กลับ​คืน​มา​ทันที และ​ทีม​ต่างๆ จะ​เริ่ม​ทำงาน​ทับซ้อน​กัน​เวลา​แก้ code พร้อม​กัน

CatalogBoundedContextController — ดึง​ข้อมูล​จาก context ตัวเอง​เท่านั้น
[ChildActionOnly] // render เป็นหน้าเดี่ยว ๆ ไม่ได้ — เป็นชิ้นส่วนเท่านั้น
public PartialViewResult ItemInBasket(string productId)
{
// ถามจาก ProductFinder ของ Catalog context เท่านั้น
var product = CatalogBoundedContext.ProductFinder.Find(productId);
// convention หา partial view ที่ /Views/CatalogBoundedContext/ItemInBasket.cshtml
return PartialView(product);
}

PartialView() จะ​คืน HTML ที่​เกิด​จาก Razor partial view เฉพาะ​ของ context นั้น แล้ว​ถูก​เอา​ไป​แปะ​ลง​ตรง​ตำแหน่ง​ที่​เรียก RenderAction() พอดี เมื่อ​รัน​แล้ว​เปิด​หน้า​แรก เรา​จะ​ได้​หน้า​ตะกร้า​ที่​ประกอบ​จาก3 context ดัง​รูป​ที่ 23-8

รูปที่ 23-8: หน้า composite UI ที่ render ออกมาแล้ว — รายการสินค้า ราคา และตัวเลือกการจัดส่ง ถูกประกอบเป็นหน้าเดียวจากชิ้นส่วน HTML ของแต่ละ context

🛒 มอง​ด้วย​ตัวอย่าง e-commerce

หน้า​ตะกร้า​หนึ่ง​หน้า = 3 context ทำงาน​ร่วม​กัน: Catalog ส่ง​ชื่อ/รูป/คำ​อธิบาย​สินค้า, Pricing ส่ง​ราคา, Shipping ส่งตัว​เลือก​การ​จัด​ส่ง แต่ละ context ควบคุม​หน้าตา​ส่วน​ของ​ตัวเอง ถ้า​ทีม Pricing อยาก​เพิ่ม badge “ลด​ราคา” ก็​แก้​ได้​ใน​ส่วน​ของ​ตัวเอง​โดย​ไม่​ต้อง​ไป​ยุ่ง​กับ Catalog หรือ Shipping เลย


ตัวอย่าง​นี้​สลับ​ขั้ว​ทุก​แกน: ข้อมูล​มา​จาก context ที่ กระจาย (รัน​เป็น​คนละ app), ส่ง​กลับ​เป็น JSON (data API), และ​ประกอบ ฝั่ง client ด้วย JavaScript ใน​เบราว์เซอร์

รูปที่ 23-9: design ของตัวอย่างที่ 2 — หน้าค้นหา package ทัวร์ยิง AJAX HTTP/JSON ไปยัง3 context ที่เขียนคนละภาษากัน (Discovery=C#, Promotions=Scala/JVM, Recommendations=Ruby on Rails)

จุด​สำคัญ​ใน​รูป​ที่ 23-9: แต่ละ API อยู่​ใน Bounded Context ที่ ไม่รู้จัก context อื่น​เลย คุณ​จึง​เขียน​แต่ละ context ด้วย​เทคโนโลยี​คนละ​อย่าง​ได้ — Discovery เป็น C#, Promotions เป็น Scala/JVM, Recommendations เป็น Ruby on Rails — ขอ​แค่​ทุก​ตัว​เปิด HTTP API ตาม​ที่​ตกลง​กัน และ​เพราะ​แต่ละ API คืน​แค่ JSON เรื่อง presentation ทั้งหมด​จึง​ถูกรวบ​ไว้​ใน​เว็บไซต์​หลัก​ที่​เดียว

pppddd-application.js — ประกอบ​ฝั่ง client ด้วย AJAX
$(document).ready(function () {
// ยิงไปแต่ละ context อย่างอิสระ แล้วค่อยเติมผลลงหน้าจอ
$.getJSON("/holidays", (json) =>
json.forEach((h) => $("#holidays").append(createHolidayView(h))));
$.getJSON("/promotions", (json) =>
json.forEach((h) => $("#promotions").append(createHolidayView(h))));
$.getJSON("/recommendations", (json) =>
json.forEach((h) => $("#recommendations").append(createHolidayView(h))));
});
// presentation logic อยู่ที่ client ทั้งหมด — context ส่งมาแค่ data
function createHolidayView(holiday) {
return `<div><img src="${holiday.ImgUrl}" height="75"/>
<h4>${holiday.Title}</h4>$${holiday.Price}pp</div>`;
}

สังเกต​ว่า function createHolidayView() คือ​ที่​ที่ HTML ถูก​สร้าง​ขึ้น — presentation logic ทั้งหมด​อยู่​ใน​เว็บ app ไม่​ได้ กระจาย​ไป​อยู่​ใน​แต่ละ context (ซึ่ง​จะ​เป็น​กรณี​ตรง​ข้าม​ถ้า API คืน HTML) นี่​ไม่​ดี​หรือ​แย่​กว่า — มัน​คือ​ทาง​เลือก​ที่​ต้อง​ตัดสิน​ใจ​ตาม trade-off

รูปที่ 23-10: ผลลัพธ์ของการประกอบฝั่ง client ด้วย JSON API — ผลค้นหา, โปรโมชั่น และคำแนะนำ ถูกเติมลงหน้าจอแบบ async จาก3 context ที่กระจายกัน

ทาง​ลัด​ใน​ตัวอย่าง​ที่​อย่า​ทำ​ใน​งาน​จริง

ตัวอย่าง​นี้​บังคับ​ให้​ทุก API คืน JSON รูปแบบ​เดียวกัน​ทุก​ประการ​เพื่อ​ความ​ง่าย แต่​ใน​ระบบ​จริง ไม่​ควร​ทำ เพราะ​มัน​สร้าง coupling ข้าม context โดย​ไม่​จำเป็น และ​การ​ต่อ string HTML ใน createHolidayView() ก็​ควร​เปลี่ยน​ไป​ใช้ templating library (เช่น Handlebars หรือ framework อย่าง Angular/Knockout) ใน​งาน​จริง

แต่ละ controller ใน​ตัวอย่าง​ทำ​แค่​ขั้น​ต่ำ​เพื่อ​คืน JSON ใน​งาน​จริง ตรง​นี้​คือ​ที่​ที่ Application ServiceApplication Serviceบริการ​ที่ “ประสาน​งาน” use case — จัดการ transaction, ความ​ปลอดภัย, การ​บันทึก, แปลง DTO — แต่​ไม่มี​กฎ​ธุรกิจ​อยู่​ใน​ตัว​มัน​เองTactical Design เข้า​มา​ทำงาน — มัน​นั่ง​อยู่​ระหว่าง domain กับ external contract (เช่น HTTP API) คอย​ประสาน​งาน​ตาม input ที่​เข้า​มา​จาก UI หรือ output ที่​จะ​ส่ง​ออก​ไป​ให้ UI ซึ่ง​เป็น​เนื้อหา​ของ​บท​ถัด​ไป

🚢 เทียบ​กับ​ระบบ​ขนส่ง​สินค้า

ลอง​นึกถึง​หน้า “ติดตาม​สถานะ​ตู้ container” ที่​ต้อง​รวม​ข้อมูล​จาก​หลาย context: Booking (ข้อมูล​การ​จอง), Routing (แผน​เดินทาง ItineraryItineraryValue Object ที่​เป็น “แผนการ​เดินทาง​จริง” ของ Cargo ประกอบ​ด้วย​ลำดับ​ของ Leg (ช่วง​การ​ขนส่ง​แต่ละ​ช่วง)Tactical Design), และ Handling (Handling EventHandling Eventการ​บันทึก​การ​จัดการ​สินค้า​จริง (LOAD, UNLOAD, RECEIVE, CLAIM) เป็น Aggregate “แยก​ต่างหาก” จาก Cargo เพราะ​มี​ปริมาณ​มาก​และ​ต้อง​ประมวล​ผล​แบบ asyncTactical Design ล่าสุด) เรา​เลือก​ได้​ว่า​จะ​ให้ Tracking context เก็บ copy ข้อมูล​ทั้งหมด​ไว้​ล่วงหน้า (autonomous, ยอมรับ eventual consistency) หรือ​จะ​ยิง​ถาม​แต่ละ context ตอน​เปิด​หน้า (authoritative, ข้อมูล​สด​เสมอ) — เป็น​แกน​การ​ตัดสิน​ใจ​ชุด​เดียว​กับ​ที่​เรา​เรียน​ใน​บท​นี้​ทุก​ประการ


  • การ​จัด​วาง Bounded Context หลัง​บ้าน​มี​อิทธิพล​อย่าง​มาก​ต่อ UI และ​ใน​ทาง​กลับ​กัน UI ก็​อาจ​กำหนด​ว่า context ใด​ต้อง​เก็บ​ข้อมูล​อะไร
  • การ​ตัดสิน​ใจ​ว่า​ทีม​ไหน​เป็น​เจ้าของ UI ส่ง​ผล​ทั้ง​ต่อ​พลวัต​ของ​ทีม​และ​ทางออก​ทาง​วิศวกรรม
  • ประกอบ​ข้อมูล​ได้​ทั้ง ฝั่ง client (JavaScript — ลด coupling และ component ตัวกลาง) หรือ ฝั่ง server (ตัด dependency กับ JavaScript และ​ข้อ​จำกัด​ของ browser)
  • ส่ง​กลับ​ได้​ทั้ง HTML (ให้​อำนาจ presentation กับ​แต่ละ context แต่​กระจัดกระจาย) หรือ data/JSON (รวบ presentation ที่​เดียว แต่​ดึง​สิทธิ์​นั้น​ไป​จาก context)
  • ไม่มี​แบบ​ไหน “ถูกต้อง​ที่สุด” โดย​สมบูรณ์ — ทุก​อย่าง​คือ trade-off ที่​ต้อง​เลือก​อย่าง​ตั้งใจ​ตาม​บริบท​ของ​คุณ
หัวใจ​ของ​บท​นี้

UI ที่​ดึง​ข้อมูล​จาก​หลาย Bounded Context ต้อง​ตัดสิน​ใจ​สาม​แกน: ใคร​เป็น​เจ้าของ (autonomous เก็บ local vs authoritative ถาม​ต้นทาง), ส่ง​อะไร​กลับ (HTML vs data API), และ ประกอบ​ที่ไหน (client vs server) ทั้งหมด​นี้​อยู่​ภาย​ใต้​กฎ​เหล็ก​ข้อ​เดียว​ที่​คุม​ทุก​แกน — ห้าม​เจาะ​ฐาน​ข้อมูล​ของ context อื่น​โดยตรง​เด็ดขาด นี่​คือ​เส้น​ที่​ขีด​ไว้​เพื่อ​รักษา loose coupling เอา​ไว้​ให้​คง​อยู่

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

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

  • Bounded Context — พื้นฐาน​ของ​ทั้ง​บท: ทุก​การ​ตัดสิน​ใจ​เรื่อง UI composition ล้วน​หมุน​รอบ​การ​รักษา​ขอบเขต​ของ context แต่ละ​อัน​ไม่​ให้​ผูก​กัน​แน่น
  • Domain Events — กลไก​เบื้องหลัง UI แบบ autonomous ที่ subscribe event จาก context อื่น​มา​เก็บ copy ไว้​ล่วงหน้า
  • MVC — ตัวอย่าง​ที่ 1 ใช้ RenderAction() ของ ASP.NET MVC เป็น​กลไก​ประกอบ HTML ฝั่ง server จาก​หลาย controller

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

ข้อ 1 / 4

ทำไมเว็บ app จึง 'ไม่ควร' query ตรงเข้าฐานข้อมูลของ Bounded Context อื่นเพื่อประกอบ UI?