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 เดียวกัน
บทบาทใน DDD
หัวข้อที่มีชื่อว่า “บทบาทใน DDD”Shared Kernel อยู่ในระดับ Strategic Design เช่นเดียวกับ Context Mapping ทั้งหมด บทบาทของมันคือการตอบคำถามว่า “เมื่อ2 bounded context มีส่วนที่เหมือนกันมากจริง ๆ เราควรจัดการอย่างไร” มีสามทางเลือกหลักที่ Context Mapping เสนอไว้ให้เปรียบเทียบ:
- แยก model ให้เป็นอิสระเต็มที่ (Separate Ways หรือใช้ Anticorruption Layer แปลระหว่างกัน) — ปลอดภัยแต่ต้องดูแล model ซ้ำซ้อนสองชุด
- ให้ฝั่งหนึ่งยอมตามอีกฝั่ง (Conformist หรือ Customer/Supplier) — ลดงานแปล แต่เสียอิสระในการออกแบบ
- แบ่งปัน 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 กับ Billingpublic 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 โดยตรง ไม่ต้องแปล modelpublic 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 ที่ดูแลร่วมกันต้องอาศัยการสื่อสารระหว่างทีมที่ใกล้ชิดกว่าปกติ