Ubiquitous Language
ทุกฝ่ายใช้คำศัพท์ชุดเดียวกันในการพูดถึงระบบ
Ubiquitous Language คือคำที่ Eric Evans บัญญัติไว้ใน Domain-Driven Design (2003) หมายถึงภาษาที่ สร้างขึ้นร่วมกัน ระหว่างทีมพัฒนากับผู้เชี่ยวชาญ domain (domain expert) แล้วนำไปใช้ “อย่างทั่วถึง” (pervasively) ทั้งในบทสนทนา เอกสารประกอบ ไดอะแกรม test และที่สำคัญที่สุดคือ ใน code เอง — ชื่อ class เมทอด และตัวแปรต้องสะท้อนคำศัพท์เดียวกับที่ผู้เชี่ยวชาญ domain ใช้พูดในห้องประชุม
จุดเน้นสำคัญตามที่ Evans ระบุคือภาษานี้ต้อง ผูกกับ domain model ไม่ใช่แค่ศัพท์ทางธุรกิจลอย ๆ เพราะซอฟต์แวร์ทนความกำกวมไม่ได้ — ถ้าคำ ๆ หนึ่งถูกใช้สื่อความหมายต่างกันในบริบทต่างกัน model จะพังและ code จะเต็มไปด้วย if-else ที่พยายามประนีประนอมความหมายที่ขัดแย้งกัน Martin Fowler สรุปไว้ในบทความ bliki ของเขาว่าภาษานี้ควรอยู่บนฐานของ model เพื่อให้ใช้ทดสอบความถูกต้องของ model นั้นได้ในตัว — เมื่อคุยกับผู้เชี่ยวชาญ domain ด้วยภาษานี้แล้วรู้สึกฝืดเคือง นั่นคือสัญญาณว่า model ยังไม่สะท้อนความเป็นจริง
ภาษานี้ ไม่หยุดนิ่ง — มันพัฒนาไปพร้อมกับความเข้าใจ domain ที่ลึกขึ้นของทีม ผู้เชี่ยวชาญ domain ควรทักท้วงเมื่อคำที่ใช้ฟังดูขัดหูหรือไม่ครอบคลุม ส่วนนักพัฒนาก็ควรชี้จุดที่คำกำกวมออกมา แล้วปรับ Ubiquitous Language พร้อมกับปรับ code ไปด้วยกัน
บทบาทใน DDD
หัวข้อที่มีชื่อว่า “บทบาทใน DDD”- สะพานเชื่อมระหว่างธุรกิจกับ code — ลดต้นทุนการ “แปล” ระหว่างศัพท์ธุรกิจกับศัพท์เทคนิค เมื่อนักวิเคราะห์ธุรกิจพูดว่า “จองห้อง” นักพัฒนาก็ implement เมทอดชื่อ
Reserveไม่ใช่SetStatus(2)หรือUpdate() - มีขอบเขตเป็น Bounded Context — Ubiquitous Language ไม่ใช่ภาษาสากลทั้งองค์กร แต่ valid เฉพาะภายใน Bounded Context หนึ่ง ๆ คำเดียวกัน เช่น “Customer” อาจมีความหมายและคุณสมบัติต่างกันโดยสิ้นเชิงใน context ของ Sales กับ context ของ Support — พยายามยัดทุกความหมายไว้ใน model เดียวคือสูตรของ Big Ball of Mud
- ค้นพบผ่านการทำงานร่วมกัน ไม่ใช่กำหนดจากบนลงล่าง — เทคนิคอย่าง Event Storming และ Domain Storytelling เป็นเวิร์กช็อปที่ออกแบบมาเพื่อดึงภาษานี้ออกมาจากผู้เชี่ยวชาญ domain โดยตรง แทนที่จะให้นักพัฒนานั่งเดาคำเอง
- ขับเคลื่อนการตั้งชื่อใน code — งานตั้งชื่อ (ดู Naming Things) เป็นหนึ่งในงานยากที่สุดของการเขียนโปรแกรม การมีภาษากลางที่ตกลงกันแล้วทำให้ทีมไม่ต้องเถียงกันเองว่า class ควรชื่ออะไร เพราะคำตอบมาจาก domain อยู่แล้ว
- ปรากฏใน Domain Event — ตาม convention ที่ใช้กันทั่วไป (เช่นในแนวทางของ Microsoft) ชื่อ event ควรเป็นกริยารูปอดีตที่ตรงกับภาษาที่ผู้เชี่ยวชาญ domain ใช้พูดถึงสิ่งที่ “เกิดขึ้นแล้ว” เช่น
OrderShippedหรือPaymentDeclined— ไม่ใช่ชื่อกลาง ๆ อย่างStatusChanged - ผลประโยชน์ที่แท้จริงมาจากการทำงานใกล้ชิด — Pair Programming และแนวทาง Whole Team ทำให้ภาษาถูกตกลงร่วมกันได้เร็วและสม่ำเสมอ ตรงข้ามกับชื่อที่มาจากทีมการตลาดหรือไดอะแกรมยุคแรกที่ไม่เคยถูกเทียบกับ code จริง
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”สมมติผู้เชี่ยวชาญ domain ของธุรกิจขนส่งพูดประโยคเหล่านี้ในเวิร์กช็อป:
“เมื่อลูกค้า วางคำสั่งซื้อ (place an order) ระบบจะ จอง สต็อกไว้ก่อน จนกว่าคลังจะ จัดส่ง (ship) แล้วถึง ยืนยันการจัดส่ง ได้ ถ้าลูกค้าเปลี่ยนใจก่อนจัดส่ง เขาสามารถ ยกเลิก คำสั่งซื้อได้ แต่ถ้าจัดส่งไปแล้วต้อง ขอคืนสินค้า (return) แทน”
คำที่ตัวหนา — place, reserve, ship, cancel, return — คือ Ubiquitous Language ที่ต้องปรากฏตรงตัวใน code ไม่ใช่ถูกลดทอนเหลือแค่ enum สถานะกับเมทอด Update เดียว:
// สะท้อน Ubiquitous Language ตรง ๆ ในชื่อเมทอดและชื่อ eventpublic class Order{ public OrderId Id { get; } public OrderStatus Status { get; private set; } private readonly List<OrderLine> _lines = new();
private Order(OrderId id, IEnumerable<OrderLine> lines) { Id = id; _lines.AddRange(lines); Status = OrderStatus.Placed; }
// "วางคำสั่งซื้อ" ในภาษาธุรกิจ = factory method ชื่อ Place public static Order Place(OrderId id, IEnumerable<OrderLine> lines) => new Order(id, lines);
// "จอง" สต็อก คือพฤติกรรมที่ชัดเจน ไม่ใช่แค่เปลี่ยนตัวเลขสถานะ public void Reserve() { if (Status != OrderStatus.Placed) throw new InvalidOperationException("จองได้เฉพาะคำสั่งซื้อที่เพิ่งวางเท่านั้น");
Status = OrderStatus.Reserved; }
// "จัดส่ง" คือคำกริยาที่ผู้เชี่ยวชาญ domain ใช้ ไม่ใช่ SetStatus(3) public OrderShipped Ship(TrackingNumber tracking) { if (Status != OrderStatus.Reserved) throw new InvalidOperationException("จัดส่งได้เฉพาะคำสั่งซื้อที่จองไว้แล้ว");
Status = OrderStatus.Shipped; // ชื่อ domain event เป็นกริยารูปอดีต ตรงตาม ubiquitous language return new OrderShipped(Id, tracking); }
// "ยกเลิก" ใช้ได้เฉพาะก่อนจัดส่งเท่านั้น ตามที่ domain กำหนดไว้ public void Cancel() { if (Status == OrderStatus.Shipped) throw new InvalidOperationException("จัดส่งไปแล้วต้องใช้ Return แทน Cancel");
Status = OrderStatus.Cancelled; }
// หลังจัดส่งแล้ว การเปลี่ยนใจเรียกว่า "คืนสินค้า" ไม่ใช่ "ยกเลิก" public void Return() { if (Status != OrderStatus.Shipped) throw new InvalidOperationException("คืนสินค้าได้เฉพาะคำสั่งซื้อที่จัดส่งไปแล้ว");
Status = OrderStatus.Returned; }}
public record OrderShipped(OrderId OrderId, TrackingNumber Tracking);สังเกตว่า Cancel กับ Return คือสองเมทอดที่แยกกันชัดเจน ทั้งที่ทางเทคนิคอาจ “เปลี่ยนสถานะเป็นไม่ใช้งาน” เหมือนกัน — แต่ในภาษาธุรกิจมันคือคนละเรื่องกัน (ก่อน vs หลังจัดส่ง) การมีเมทอดแยกทำให้ code อ่านแล้วตรงกับที่ผู้เชี่ยวชาญ domain พูด และป้องกัน invariant ผิดที่เนียนอยู่ในเงื่อนไข if ซ้อนกัน
แผนภาพต่อไปนี้แสดงว่า Ubiquitous Language อยู่ตรงกลางระหว่างผู้คนกับสิ่งประดิษฐ์ทางเทคนิค และถูกกำกับขอบเขตด้วย Bounded Context:
classDiagram
class DomainExpert
class Developer
class UbiquitousLanguage
class BoundedContext
class DomainModel
class SourceCode
class DomainEvent
DomainExpert --> UbiquitousLanguage
Developer --> UbiquitousLanguage
BoundedContext o-- UbiquitousLanguage
UbiquitousLanguage --> DomainModel
DomainModel --> SourceCode
DomainModel --> DomainEvent
ความสัมพันธ์กับแนวคิดอื่น
หัวข้อที่มีชื่อว่า “ความสัมพันธ์กับแนวคิดอื่น”- Bounded Context — Ubiquitous Language ใช้ได้เฉพาะภายในขอบเขตเดียว คนละ context อาจใช้คำเดียวกันแต่ความหมายต่างกัน ยิ่งองค์กรใหญ่ ยิ่งมีหลายภาษากลางคู่ขนานกัน ไม่ใช่ภาษาเดียวทั้งบริษัท
- Context Map — เมื่อ2 context ต้องสื่อสารกัน Context Map คือเครื่องมือบันทึกว่าคำศัพท์และ model ของแต่ละฝั่งแปลงหากันอย่างไร (เช่นผ่าน Anticorruption Layer)
- Domain Model — ภาษาต้อง “ผูกกับ model” เสมอ model คือสิ่งที่ทำให้ภาษารัดกุมพอจะเขียนเป็น code ได้ ถ้าแยกภาษาออกจาก model มันจะเหลือแค่ศัพท์การตลาดที่ไม่มีความหมายทางเทคนิค
- Event Storming และ Domain Storytelling — เทคนิคเวิร์กช็อปที่ใช้ค้นหาและกลั่นกรอง Ubiquitous Language ร่วมกับผู้เชี่ยวชาญ domain ก่อนนำไปเขียนเป็น model
- Aggregate และ Entity — ชื่อ class เมทอด และ invariant ของ aggregate/entity ควรยกมาจาก Ubiquitous Language ตรง ๆ ตามตัวอย่าง
Order.Ship()ข้างต้น - Naming Things — Ubiquitous Language คือแหล่งคำตอบเวลาตั้งชื่อ ลดการถกเถียงเรื่องชื่อที่ไม่มีหลักอ้างอิง
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/domain-driven-design/ubiquitous-language
- Ubiquitous Language — Martin Fowler’s bliki
- Designing a DDD-oriented microservice — Microsoft Learn (.NET Microservices Architecture)
- Domain events: Design and implementation — Microsoft Learn
- Implementing Domain-Driven Design — Vaughn Vernon, sample pages (Pearson)