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

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 ไป​ด้วย​กัน

  1. สะพาน​เชื่อม​ระหว่าง​ธุรกิจ​กับ code — ลด​ต้นทุน​การ “แปล” ระหว่าง​ศัพท์​ธุรกิจ​กับ​ศัพท์​เทคนิค เมื่อ​นัก​วิเคราะห์​ธุรกิจ​พูด​ว่า “จอง​ห้อง” นัก​พัฒนา​ก็ implement เมทอด​ชื่อ Reserve ไม่ใช่ SetStatus(2) หรือ Update()
  2. มี​ขอบเขต​เป็น Bounded Context — Ubiquitous Language ไม่ใช่​ภาษา​สากล​ทั้ง​องค์กร แต่ valid เฉพาะ​ภายใน Bounded Context หนึ่ง ๆ คำ​เดียวกัน เช่น “Customer” อาจ​มี​ความหมาย​และ​คุณสมบัติ​ต่าง​กัน​โดย​สิ้นเชิง​ใน context ของ Sales กับ context ของ Support — พยายาม​ยัด​ทุก​ความหมาย​ไว้​ใน model เดียว​คือ​สูตร​ของ Big Ball of Mud
  3. ค้น​พบ​ผ่าน​การ​ทำงาน​ร่วม​กัน ไม่ใช่​กำหนด​จาก​บน​ลง​ล่าง — เทคนิค​อย่าง Event Storming และ Domain Storytelling เป็น​เวิร์กช็อป​ที่​ออกแบบ​มา​เพื่อ​ดึง​ภาษา​นี้​ออก​มา​จาก​ผู้เชี่ยวชาญ domain โดยตรง แทนที่​จะ​ให้​นัก​พัฒนา​นั่ง​เดา​คำ​เอง
  4. ขับ​เคลื่อน​การ​ตั้ง​ชื่อ​ใน code — งาน​ตั้ง​ชื่อ (ดู Naming Things) เป็น​หนึ่ง​ใน​งาน​ยาก​ที่สุด​ของ​การ​เขียน​โปรแกรม การ​มี​ภาษา​กลาง​ที่​ตกลง​กัน​แล้ว​ทำให้​ทีม​ไม่​ต้อง​เถียง​กันเอง​ว่า class ควร​ชื่อ​อะไร เพราะ​คำ​ตอบ​มา​จาก domain อยู่​แล้ว
  5. ปรากฏ​ใน Domain Event — ตาม convention ที่​ใช้​กัน​ทั่วไป (เช่น​ใน​แนวทาง​ของ Microsoft) ชื่อ event ควร​เป็นกริยา​รูป​อดีต​ที่​ตรง​กับ​ภาษา​ที่​ผู้เชี่ยวชาญ domain ใช้​พูด​ถึง​สิ่ง​ที่ “เกิด​ขึ้น​แล้ว” เช่น OrderShipped หรือ PaymentDeclined — ไม่ใช่​ชื่อ​กลาง ๆ อย่าง StatusChanged
  6. ผล​ประโยชน์​ที่แท้​จริง​มา​จาก​การ​ทำงาน​ใกล้​ชิดPair Programming และ​แนวทาง Whole Team ทำให้​ภาษา​ถูก​ตกลง​ร่วม​กัน​ได้​เร็ว​และ​สม่ำเสมอ ตรง​ข้าม​กับ​ชื่อ​ที่มา​จาก​ทีม​การ​ตลาด​หรือ​ไดอะแกรม​ยุค​แรก​ที่​ไม่​เคย​ถูก​เทียบ​กับ code จริง

สมมติ​ผู้เชี่ยวชาญ domain ของ​ธุรกิจ​ขนส่ง​พูด​ประโยค​เหล่า​นี้​ใน​เวิร์กช็อป:

“เมื่อ​ลูกค้า วาง​คำ​สั่ง​ซื้อ (place an order) ระบบ​จะ จอง สต็อก​ไว้​ก่อน จนกว่า​คลัง​จะ จัด​ส่ง (ship) แล้ว​ถึง ยืนยัน​การ​จัด​ส่ง ได้ ถ้า​ลูกค้า​เปลี่ยนใจ​ก่อน​จัด​ส่ง เขา​สามารถ ยกเลิก คำ​สั่ง​ซื้อ​ได้ แต่​ถ้า​จัด​ส่ง​ไป​แล้ว​ต้อง ขอ​คืน​สินค้า (return) แทน”

คำ​ที่​ตัว​หนา — place, reserve, ship, cancel, return — คือ Ubiquitous Language ที่​ต้อง​ปรากฏ​ตรง​ตัว​ใน code ไม่ใช่​ถูกลด​ทอน​เหลือ​แค่ enum สถานะ​กับ​เมทอด Update เดียว:

// สะท้อน Ubiquitous Language ตรง ๆ ในชื่อเมทอดและชื่อ event
public 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 คือ​แหล่ง​คำ​ตอบ​เวลา​ตั้ง​ชื่อ ลด​การ​ถกเถียง​เรื่อง​ชื่อ​ที่​ไม่มี​หลัก​อ้างอิง