ออกแบบ User Interface ของ application
ลูกค้าส่วนใหญ่สนใจแค่ หน้าจอ ของ application เรา ถ้ามันสวย ใช้ง่าย และพาเขาไปถึงสิ่งที่ต้องการได้ — เช่น พบ package ทัวร์ในฝัน — เขาก็ยินดีจ่ายเงิน แต่การทำ UI ให้ดีนั้นไม่ใช่แค่เรื่องของดีไซเนอร์ที่รังสรรค์ภาพสวยๆ เพราะเบื้องหลังยังมีโจทย์วิศวกรรมที่หนักหน่วงซ่อนอยู่ ทั้งเรื่อง performance, scalability และ loose coupling ของ Bounded ContextBounded Contextขอบเขตที่ชัดเจน (มักเป็นระบบย่อยหรือทีมหนึ่ง) ที่ model หนึ่งใช้ได้และมีความหมายแน่นอน ภายในขอบเขตนี้ Ubiquitous Language สอดคล้องกัน 100%Strategic Design ที่ทำงานอยู่หลังฉาก
บทนี้เราจะตอบคำถามที่ฟังดูง่ายแต่จริงๆ แล้วยาก: เมื่อข้อมูลกระจัดกระจายอยู่หลาย Bounded Context หน้า UI เดียวจะ “ประกอบร่าง” ข้อมูลทั้งหมดเข้าด้วยกันได้อย่างไร โดยไม่ทำให้ระบบกลับมาผูกกันแน่น (coupling) อีกครั้ง
ทำไมการประกอบ UI ถึงยาก
หัวข้อที่มีชื่อว่า “ทำไมการประกอบ UI ถึงยาก”ใน 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 ของมัน เราจะแบ่งการตัดสินใจออกเป็นสามแกนหลัก แล้วปิดท้ายด้วยตัวอย่างจริงสองชุด
แกนที่ 1: Autonomous vs Authoritative — ใครเป็นเจ้าของ UI
หัวข้อที่มีชื่อว่า “แกนที่ 1: Autonomous vs Authoritative — ใครเป็นเจ้าของ UI”คำถามแรกในการออกแบบ UI คือ “ในเชิงตรรกะแล้ว ใครเป็นเจ้าของหน้านี้?” หน้าจออาจอยู่ภายใน Bounded Context เดียวและเป็นของทีมนั้น หรืออาจดึงข้อมูลจากหลาย context แต่ไม่ได้เป็นของใครเลยก็ได้
Autonomous — เก็บทุกอย่างไว้กับตัว
หัวข้อที่มีชื่อว่า “Autonomous — เก็บทุกอย่างไว้กับตัว”UI แบบ autonomous เป็นของ business component เดียว ไม่ต้องไปดึงข้อมูลจาก context อื่นตอน render เลย แต่นั่นก็แปลว่า context นี้ต้อง เก็บข้อมูลทุกอย่างที่จะแสดงบนหน้าจอไว้กับตัวเอง วิธีทำคือ subscribe รับ Domain EventDomain Eventสิ่งที่ “เกิดขึ้นแล้ว” ใน domain ซึ่งส่วนอื่นสนใจ แทนด้วยอ็อบเจ็กต์ที่เปลี่ยนแปลงไม่ได้ ตั้งชื่อเป็นอดีต เช่น CargoWasRouted ใช้สื่อสารข้าม AggregateTactical Design จาก context อื่น แล้วเก็บสำเนา (copy) ของข้อมูลที่ต้องใช้ไว้ใน local

ในรูปที่ 23-1 ทีม catalog มีหน้าเครื่องมือสำหรับปรับแต่งคอนเทนต์ของสินค้า พวกเขาอยากรู้ว่าสินค้าตัวไหนขายดีแค่ไหน เพื่อจะได้ทุ่มแรงทำคอนเทนต์ให้ถูกตัว ข้อมูลยอดขายนี้ได้มาจากการ subscribe รับ Sale Completed event จาก Sales context แล้วเก็บไว้ใน Content Enhancement DB — พร้อมแสดงทันทีโดยไม่ต้องไปถามใคร
ข้อควรระวังคือ Eventual ConsistencyEventual Consistencyความถูกต้องที่ “ตามมาทีหลัง” ไม่ใช่ทันที ใช้กับการอัปเดตข้าม Aggregate ผ่าน Domain Event แทนการบังคับให้ตรงกันในทันทีภายใน transaction เดียวTactical Design: ข้อมูลที่แสดงอาจไม่ใช่ค่าล่าสุดอย่างแม่นยำ สำหรับกรณีนี้ไม่เป็นไร — ตัวเลขยอดขายที่ช้าไปไม่กี่นาที กี่ชั่วโมง หรือแม้แต่กี่วันก็ยังใช้ตัดสินใจได้ แต่ถ้า ความสดใหม่ของข้อมูลสำคัญมาก autonomous อาจไม่ใช่ทางเลือกที่ดี
Authoritative — ถามจากต้นทางทุกครั้ง
หัวข้อที่มีชื่อว่า “Authoritative — ถามจากต้นทางทุกครั้ง”เมื่อคุณต้องการ snapshot ล่าสุดจริงๆ จากหลาย context ในหน้าเดียว app ต้อง ร้องขอข้อมูลแต่ละชิ้นตรงไปยัง context ที่เป็นเจ้าของข้อมูลนั้น (authority) ทุกครั้งที่โหลดหน้า

จุดสำคัญในรูปที่ 23-2: หน้าเว็บนี้ ไม่ได้เป็นของ Bounded Context ใดเลย หลายบริษัทจึงมีทีมเว็บโดยเฉพาะที่ไม่ได้เป็นเจ้าของ context ไหน แต่รับผิดชอบเว็บไซต์ทั้งหมด หรือถ้าจะให้ทีมที่เป็นเจ้าของ context หนึ่งมาดูแลหน้านี้ก็ได้ ขอแค่จำไว้ว่า ในเชิงแนวคิด หน้านี้ไม่ใช่ส่วนหนึ่งของ context นั้น
ดูที่ ความเป็นเจ้าของของทีม และ ความสดใหม่ของข้อมูล: ถ้าเป็นเครื่องมือภายในของแผนกใดแผนกหนึ่งและข้อมูลช้าได้บ้าง → autonomous (เก็บ local) คุ้มกว่า แต่ถ้าเป็นส่วนหนึ่งของเว็บสาธารณะที่ต้องการข้อมูลล่าสุดเสมอ → authoritative (ถามต้นทาง) เหมาะกว่า อย่าลงทุนเก็บข้อมูลซ้ำซ้อนมหาศาลเพียงเพื่อ use case เล็กๆ
แกนที่ 2: HTML API vs Data API — ส่งอะไรกลับมา
หัวข้อที่มีชื่อว่า “แกนที่ 2: HTML API vs Data API — ส่งอะไรกลับมา”เมื่อตัดสินใจแล้วว่าจะดึงข้อมูลจากหลาย 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)
HTML มอบอำนาจการแสดงผลให้แต่ละ context (เปลี่ยนหน้าตาส่วนของตัวเองได้โดยไม่กระทบใคร) แต่ทำให้ presentation logic กระจัดกระจาย — ส่วน Data/JSON รวบ presentation มาไว้ที่เดียว ดูแลง่ายและสม่ำเสมอ แต่ context จะไม่มีสิทธิ์กำหนดหน้าตาส่วนของตัวเอง ไม่มีแบบไหน “ดีกว่า” โดยสมบูรณ์ มันคือการเลือกตาม trade-off
แกนที่ 3: ประกอบที่ Client หรือ Server
หัวข้อที่มีชื่อว่า “แกนที่ 3: ประกอบที่ Client หรือ Server”เมื่อต้องดึงเนื้อหา (ไม่ว่าจะ 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)

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

หน้าตะกร้าหนึ่งหน้า = 3 context ทำงานร่วมกัน: Catalog ส่งชื่อ/รูป/คำอธิบายสินค้า, Pricing ส่งราคา, Shipping ส่งตัวเลือกการจัดส่ง แต่ละ context ควบคุมหน้าตาส่วนของตัวเอง ถ้าทีม Pricing อยากเพิ่ม badge “ลดราคา” ก็แก้ได้ในส่วนของตัวเองโดยไม่ต้องไปยุ่งกับ Catalog หรือ Shipping เลย
ตัวอย่างที่ 2: ประกอบฝั่ง Client ด้วย Data API (context กระจาย)
หัวข้อที่มีชื่อว่า “ตัวอย่างที่ 2: ประกอบฝั่ง Client ด้วย Data API (context กระจาย)”ตัวอย่างนี้สลับขั้วทุกแกน: ข้อมูลมาจาก context ที่ กระจาย (รันเป็นคนละ app), ส่งกลับเป็น JSON (data API), และประกอบ ฝั่ง client ด้วย JavaScript ในเบราว์เซอร์

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

ตัวอย่างนี้บังคับให้ทุก 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:
- 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?