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

Shared Kernel

code และ​ข้อมูล​แกน​กลาง​ที่​ใช้​ร่วม​กัน​หลาย bounded context

Shared Kernel คือ​หนึ่ง​ใน context mapping pattern ที่ Eric Evans นิยาม​ไว้​ใน Domain-Driven Design: Tackling Complexity in the Heart of Software ว่า “กำหนด​ขอบเขต​ที่​ชัดเจน​ให้​กับ domain model ส่วน​ย่อย​ชุด​หนึ่ง​ที่​ทีม​ตกลง​จะ​แบ่งปัน​ร่วม​กัน แล้ว​รักษา kernel นี้​ให้​เล็ก​ที่สุด” ภายใน​ขอบเขต​นั้น​ให้​รวม​ทั้ง​ส่วน​ของ model code และ database design ที่​เกี่ยวข้อง​เข้า​ไว้​ด้วย และ​เนื่องจาก​ส่วน​ที่​แบ่งปัน​นี้​มี​สถานะ​พิเศษ จึง​ห้าม​แก้ไข​โดย​พลการ — ต้อง​ปรึกษา​ทีม​อีก​ฝั่ง​ก่อน​เสมอ

พูด​ให้​ง่าย​กว่า​นั้น: Shared Kernel เกิด​ขึ้น​เมื่อ bounded context สอง​แห่ง (หรือ​มากกว่า) ใน domain เดียวกัน​มี​ส่วน​ของ model ที่​ทับซ้อน​กัน​มาก​พอ​จน​การ​แยก​กัน​สร้าง​ซ้ำ (duplicate) จะ​สิ้น​เปลือง​และ​เสี่ยง​ต่อ​ความ​ไม่​สอดคล้อง​กัน​มากกว่า​การ​แบ่งปัน code ชุด​เดียวกัน ทั้ง​สอง​ทีม​จึง​ตกลง​เป็น​เจ้าของ​ร่วม​กัน​ใน​สิ่ง​ที่ ddd-crew (คลัง​อ้างอิง context-mapping บน GitHub) และ Vaughn Vernon ใน Implementing Domain-Driven Design (บท​ที่ 3) จัด​ให้​อยู่​ใน​กลุ่ม​ความ​สัมพันธ์​แบบ mutually dependent คู่​กับ Partnership — ต่าง​จาก upstream/downstream pattern อย่าง Customer/Supplier หรือ Conformist ที่​มี​ทิศทาง​อำนาจ​ต่อรอง​ไม่​เท่า​กัน

องค์​ประกอบ​ทั่วไป​ที่​มัก​อยู่​ใน shared kernel ได้แก่:

  • Ubiquitous Language ร่วม — ศัพท์​และ​แนวคิด​ที่​ทุก context (หรือ​กลุ่ม​ย่อย​ที่ share) เข้าใจ​ตรง​กัน
  • Domain Model หลัก — entity หรือ value object ที่​เป็น​แกน​กลาง​ของ​ทุก context ที่​เกี่ยวข้อง
  • Domain Logic ร่วม — กฎ​ธุรกิจ​หรือ validation ที่​ต้อง​สอดคล้อง​กัน​ทุก​ที่​ที่​ใช้
  • Data Structures — DTO, domain event, หรือ schema ฐาน​ข้อมูล​ที่​ผูก​กับ model ที่ share
  • Integration Contracts — interface ที่​กำหนด​วิธี​สื่อสาร​ระหว่าง context ที่ share kernel เดียวกัน

Shared Kernel อยู่​ใน​ระดับ Strategic Design เช่น​เดียว​กับ Context Mapping ทั้งหมด บทบาท​ของ​มัน​คือ​การ​ตอบ​คำถาม​ว่า “เมื่อ2 bounded context มี​ส่วน​ที่​เหมือน​กัน​มาก​จริง ๆ เรา​ควร​จัดการ​อย่างไร” มี​สาม​ทาง​เลือก​หลัก​ที่ Context Mapping เสนอ​ไว้​ให้​เปรียบเทียบ:

  1. แยก model ให้​เป็น​อิสระ​เต็ม​ที่ (Separate Ways หรือ​ใช้ Anticorruption Layer แปล​ระหว่าง​กัน) — ปลอดภัย​แต่​ต้อง​ดูแล model ซ้ำซ้อน​สอง​ชุด
  2. ให้​ฝั่ง​หนึ่ง​ยอม​ตาม​อีก​ฝั่ง (Conformist หรือ Customer/Supplier) — ลด​งาน​แปล แต่​เสีย​อิสระ​ใน​การ​ออกแบบ
  3. แบ่งปัน model บาง​ส่วนร่วม​กัน​เป็น Shared Kernel — ลด​งาน​ซ้ำซ้อน​และ​รับประกัน​ความ​สอดคล้อง แลก​กับ​การ​ที่​ทั้ง​สอง​ทีม​ต้อง​ประสาน​งาน​กัน​ทุก​ครั้ง​ที่​จะ​แก้ไข​ส่วน​ที่ share

Shared Kernel จึง​เหมาะ​กับ​สถานการณ์​ที่: (ก) 2 context มา​จาก​ทีม​ที่​ไว้ใจ​กัน​และ​สื่อสาร​กัน​ได้​สะดวก (ข) domain concept ที่​จะ share มี​ความ​เสถียร​สูง ไม่​ค่อย​เปลี่ยนแปลง และ (ค) การ​แปล model ไป​มาระหว่าง context จะ​สร้าง overhead หรือ​ความ​เสี่ยง​ต่อ​ความ​ไม่​สอดคล้อง​มากกว่า​ต้นทุน​ของ​การ​ประสาน​งาน

ข้อ​ควร​ระวัง​ที่ Evans, Vernon และ​แหล่ง​อ้างอิง​อื่น ๆ ย้ำ​ตรง​กัน​คือ ต้อง​รักษา kernel ให้​เล็ก เพราะ​ทุก​ครั้ง​ที่ kernel ขยาย​ตัว ความ​เป็น​อิสระ​ของ​แต่ละ​ทีม​จะ​ลด​ลง​ตาม​ไป​ด้วย และ​การ​เปลี่ยนแปลง​ใด ๆ ใน​ส่วน​ที่ share จะ​กระทบ​ทุก context ที่​พึ่งพา​มัน​พร้อม​กัน — ถ้า​ทีม​สอง​ทีม​ไม่มี affinity กัน​จริง​หรือ​ประสาน​งาน​กัน​ไม่​ดี Shared Kernel จะ​กลาย​เป็น​คอ​ขวด​และ​จุด​คู่​ควบ (coupling point) ที่​อันตราย​กว่า​การ​แยก model ตั้งแต่​แรก

สมมติ platform e-commerce มี Ordering context และ Billing context ที่​ทั้ง​คู่​ต้อง​รู้จัก​แนวคิด Money และ​กฎ​การ​คำนวณ​ส่วนลด/ภาษี​แบบ​เดียวกันเป๊ะ — ถ้า​แยก​กัน​เขียน​คนละ​ชุด ความ​เสี่ยง​คือ​วัน​หนึ่ง​กฎ​ปัดเศษ​หรือ​อัตรา​แลกเปลี่ยน​จะ​ไม่​ตรง​กัน​ระหว่าง​สอง​ระบบ ทีม​ทั้ง​สอง​จึง​ตกลง​แยก library เล็ก ๆ ชื่อ PricingKernel ออก​มา​เป็น shared kernel ที่​ทั้ง​คู่​ผนวก​เข้า​โครงการ​ของ​ตน:

graph LR
    Ordering[Ordering Context]
    Billing[Billing Context]
    Kernel[PricingKernel Shared Model]

    Ordering --> Kernel
    Billing --> Kernel
    Ordering -.->|ตกลงร่วมกันก่อนแก้ไข| Billing
// PricingKernel — library ที่ทั้ง Ordering และ Billing อ้างอิงร่วมกัน
// เจตนา: keep this kernel small ตามคำแนะนำของ Evans
// ห้ามแก้ไข file นี้ฝ่ายเดียว ต้องแจ้งอีกทีมก่อนทุกครั้ง
namespace SharedKernel.Pricing;
// Value Object กลาง ทั้ง2 context ต้องใช้ตัวเดียวกันเพื่อเลี่ยงความไม่สอดคล้อง
public readonly record struct Money(decimal Amount, string CurrencyCode)
{
public static Money Zero(string currencyCode) => new(0m, currencyCode);
public Money Add(Money other)
{
if (CurrencyCode != other.CurrencyCode)
{
throw new InvalidOperationException(
$"ไม่สามารถบวกเงินต่างสกุลได้: {CurrencyCode} กับ {other.CurrencyCode}");
}
// กฎการปัดเศษต้องเหมือนกันทุก context ที่ใช้ kernel นี้
return this with { Amount = decimal.Round(Amount + other.Amount, 2, MidpointRounding.ToEven) };
}
}
// Domain Logic ร่วม: กฎคำนวณส่วนลดที่ต้องตรงกันระหว่าง Ordering กับ Billing
public static class DiscountPolicy
{
public static Money ApplyPercentage(Money original, decimal percentage)
{
if (percentage is < 0 or > 100)
{
throw new ArgumentOutOfRangeException(nameof(percentage), "ส่วนลดต้องอยู่ระหว่าง 0-100");
}
decimal factor = 1 - (percentage / 100m);
return original with { Amount = decimal.Round(original.Amount * factor, 2) };
}
}
// ภายใน Ordering context: อ้างอิง PricingKernel โดยตรง ไม่ต้องแปล model
public sealed class OrderLine
{
public Money UnitPrice { get; }
public int Quantity { get; }
public OrderLine(Money unitPrice, int quantity)
{
UnitPrice = unitPrice;
Quantity = quantity;
}
public Money LineTotal() =>
Enumerable.Range(0, Quantity).Aggregate(Money.Zero(UnitPrice.CurrencyCode), (sum, _) => sum.Add(UnitPrice));
}
// ภายใน Billing context: ใช้ Money และ DiscountPolicy ตัวเดียวกัน
// การันตีว่าตัวเลขที่ Billing ออกใบแจ้งหนี้ตรงกับที่ Ordering แสดงให้ลูกค้าเห็นเป๊ะ
public sealed class Invoice
{
public Money Subtotal { get; }
public Invoice(Money subtotal)
{
Subtotal = subtotal;
}
public Money TotalAfterDiscount(decimal discountPercentage) =>
DiscountPolicy.ApplyPercentage(Subtotal, discountPercentage);
}

จุด​สำคัญ​ของ​ตัวอย่าง​นี้​คือ Money และ DiscountPolicy ถูก compile เป็น package เดียว เผยแพร่​ให้​ทั้ง2 context ใช้​ผ่าน dependency reference — ไม่ใช่ copy-paste คนละ​ชุด การ​เปลี่ยน​กฎ​ปัดเศษ​หรือ validation ใด ๆ ใน kernel จะ​กระทบ​ทั้ง​สอง​ทีม​พร้อม​กัน​ใน​ทันที​ที่ upgrade version จึง​ต้อง​มี​กระบวนการ​ตกลง​ร่วม​กัน (เช่น review ร่วม, versioning ที่​รอบคอบ) ก่อน merge การ​เปลี่ยนแปลง​ใด ๆ ลงใน kernel

  • Shared Kernel เป็น​หนึ่ง​ใน​รูปแบบ​ความ​สัมพันธ์​บน Context Map อยู่​ใน​กลุ่ม mutually dependent ร่วม​กับ Partnership — ต่าง​จาก Anti-Corruption Layer ที่​เลือก​รักษา​ความ​เป็น​อิสระ​ของ model โดย​ยอม​จ่าย​ต้นทุน​แปล​ภาษา​แทน​การ share code
  • เมื่อ​เทียบ​กับ Anti-Corruption Layer: ACL ใช้​เมื่อ2 context ไม่​ไว้ใจ​หรือ​ไม่​อยาก​ผูก​กับ model ของ​กัน​และ​กัน ส่วน Shared Kernel ใช้​เมื่อ2 context “ยินดี” ผูก​กัน​โดย​เจตนา เพราะ​เชื่อ​ว่า​ความ​สอดคล้อง​ที่​ได้​คุ้ม​กว่า​ต้นทุน​การ​ประสาน​งาน
  • Shared Kernel มัก​บรรจุ Ubiquitous Language ชุด​หนึ่ง​ที่​ต้อง​ตรง​กัน​ทั้ง2 context — นี่​คือ​เหตุผล​ที่​ต้อง​เลือก​เฉพาะ​แนวคิด​ที่ “เสถียร” และ​มี​ความหมาย​เดียวกัน​จริง ๆ ใน​ทุก context ที่ share ไม่ใช่​บังคับ​ให้​ทุก​อย่าง​ต้อง​ใช้​ภาษา​เดียวกัน​ทั่ว​ทั้ง​ระบบ
  • ใน​ทาง​ปฏิบัติ Shared Kernel มัก​ถูก​ใช้​ผิด​เมื่อ​ทีม​นำ​หลัก DRY มา​ใช้​เกิน​ขอบเขต bounded context — การ​หลีก​เลี่ยง code ซ้ำ​เป็น​เหตุผล​ที่​ดี แต่​ถ้า​ทำให้​ทุก context ต้อง​พึ่งพา​กัน​จน​สูญเสีย​ความ​เป็น​อิสระ ก็​ขัด​กับ​เจตนา​ดั้งเดิม​ของ Bounded Context ที่​ต้องการ​ให้​แต่ละ model พัฒนา​ไป​ตาม​จังหวะ​ของ​ตัวเอง​ได้
  • การ​ตัดสิน​ใจ​ใช้ Shared Kernel เป็น​ส่วน​หนึ่ง​ของ Strategic Design ที่​ต้อง​พิจารณา​คู่​กับ​โครงสร้าง​ทีม​จริง เพราะ kernel ที่​ดูแล​ร่วม​กัน​ต้อง​อาศัย​การ​สื่อสาร​ระหว่าง​ทีม​ที่​ใกล้​ชิด​กว่า​ปกติ