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

Anti-Corruption Layer

ชั้น​ป้องกัน​ไม่​ให้​แนวคิด​จาก​ภายนอก​รุกล้ำ domain ของ​เรา

Anti-Corruption Layer (ACL) คือ​ชุด​ของ pattern เชิง​ป้องกัน​ที่​วาง​ไว้ ระหว่าง domain model ของ​เรา​กับ bounded context อื่น​หรือ dependency ภายนอก เจตนา​ของ​ชั้น​นี้​คือ​ป้องกัน​ไม่​ให้​แนวคิด​และ model แปลกปลอม​รุกล้ำ​เข้า​มา​ปน​เปื้อน domain model

หน้าที่​ของ ACL ได้แก่: รักษา​ความ​บริสุทธิ์​ของ domain model โดย​แยก​มัน​ออก​จาก​แนวคิด​ภายนอก, ห่อ​หุ้ม​รายละเอียด​ทาง​เทคนิค ของ​ระบบ​ภายนอก​ไว้ ให้ domain โฟกัส​กับ business logic แกน​กลาง และ ส่งเสริม loose coupling ให้​เปลี่ยน​หรือ​แทนที่​ระบบ​ภายนอก​ได้​ง่าย​โดย​ไม่​กระทบ domain โดย​ทั่วไป ACL ใช้ Adapter แปลง​คำขอ​ของ domain ไป​เป็น​รูปแบบ​และ​ความหมาย​ของ​ระบบ​ภายนอก แล้ว​แปลง​ผลลัพธ์​กลับ

Eric Evans เป็น​ผู้​บัญญัติ pattern นี้​ไว้​ใน Domain-Driven Design: Tackling Complexity in the Heart of Software โดย​อธิบาย​ว่า​เมื่อ​ทีม​ต้อง​พึ่งพา​ระบบ legacy หรือ​ระบบ​ภายนอก​ที่​ควบคุม​ไม่​ได้ ทางออก​คือ​สร้าง “bubble context” ซึ่ง​เป็น​พื้นที่​สะอาด​ของ model ใหม่ แล้ว​ให้ ACL ทำ​หน้าที่​เป็น​เหมือน “สายสะดือ” ดึง​ข้อมูล​จาก​ระบบ​เดิม​ผ่าน​ตัว​แปล (translator) ก่อน​ส่ง​เข้า​สู่ model ใหม่​เสมอ ไม่มี​ทาง​ลัด​ใด​ที่​ให้​แนวคิด​ของ​ระบบ​เก่า​ไหล​ตรง​เข้า​สู่ model ใหม่​โดย​ไม่​ผ่าน​การ​แปล

Vaughn Vernon ใน Implementing Domain-Driven Design ขยาย​ความ​ว่า ใน​ฐานะ downstream client เรา​สร้าง ACL เพื่อ “ให้​บริการ function ของ​ระบบ upstream ใน​ภาษา​ของ model เรา​เอง” ชั้น​นี้​คุย​กับ​ระบบ​อื่น​ผ่าน interface ที่​มี​อยู่​เดิม​โดย​แทบ​ไม่​ต้อง​แก้ไข​ระบบ​นั้น แล้ว​แปล​ความหมาย​ไป​มาระหว่าง2 model ภายใน​ตัว​มัน​เอง

Microsoft Learn (Azure Architecture Center) นิยาม​ใน​เชิง pattern ทั่วไป​ว่า ACL คือ facade หรือ adapter layer ระหว่าง subsystem สอง​ฝั่ง​ที่​ไม่​ได้​ใช้ semantics ร่วม​กัน ปัญหา​ที่​แก้​คือ: เมื่อ migrate ระบบ legacy ไป​สู่​ระบบ​ใหม่​แบบ​ค่อย​เป็น​ค่อย​ไป feature ใหม่​มัก​ต้อง​เรียก​ระบบ​เก่า​ซึ่ง​มี schema ยุ่งเหยิง​หรือ API ล้าสมัย หาก​ปล่อย​ให้​ระบบ​ใหม่​ผูก​ติด​กับ semantics ของ​ระบบ​เก่า​โดยตรง ก็​เท่ากับ​ปล่อย​ให้​ปัญหา​คุณภาพ​ของ​ระบบ​เก่า “ปน​เปื้อน” การ​ออกแบบ​ของ​ระบบ​ใหม่

ACL เป็น​หนึ่ง​ใน context mapping pattern ที่​ใช้​บรรยาย​ความ​สัมพันธ์​ระหว่าง bounded context บน context map โดย​เฉพาะ​เมื่อ​ความ​สัมพันธ์​เป็น​แบบ downstream ที่​ทีม​เรา​ไม่มี​อำนาจ​ต่อรอง​กับ​ทีม upstream (ต่าง​จาก Conformist ที่ downstream ยอมรับ model ของ upstream ทั้งดุ้น หรือ Shared Kernel ที่​สอง​ทีม​แบ่งปัน model ร่วม​กัน) ACL คือ​ทาง​เลือก​เมื่อ​เรา​ต้องการ​รักษา​ความ​เป็น​อิสระ​ของ model ตัวเอง​ไว้ แม้​ต้อง​จ่าย​ต้นทุน​ใน​การ​แปล​ข้อมูล​ก็ตาม

ใน​ทาง​ปฏิบัติ ACL มัก​ประกอบ​ด้วย​องค์​ประกอบ​ย่อย​หลาย​ชิ้น​ทำงาน​ร่วม​กัน:

  • Facade — จุด​เข้า​เดียว​ที่​ซ่อน​ความ​ซับซ้อน​ของ​ระบบ​ภายนอก​ไว้​ทั้งหมด
  • Adapter — แปลง interface/protocol ของ​ระบบ​ภายนอก​ให้​เข้า​กับ​สิ่ง​ที่ domain คาด​หวัง
  • Translator — แปลง​ข้อมูล (DTO ↔ Domain Model) ไป​มาระหว่าง​สอง​ภาษา​ของ2 model
  • Repository/Gateway ภายใน ACL — ทำ​หน้าที่​ดึง​ข้อมูล​จาก​ระบบ​เก่า ประสาน​การ​เรียก​ใช้ function หรือ query กับ​ฐาน​ข้อมูล legacy แล้ว​ส่ง​ผล​ผ่าน translator ก่อน​คืน​ค่า​ผ่าน interface ที่ domain รู้จัก

Microsoft Learn ยัง​เตือน​ถึง​ข้อ​พิจารณา​สำคัญ​เมื่อ​นำ ACL ไป​ใช้​จริง: ชั้น​นี้​เพิ่ม latency และ​ภาระ​ใน​การ​ดูแล​รักษา ต้อง​คิด​เรื่อง scaling และ observability (correlation id, structured logging) ของ​ตัว​มัน​เอง ต้อง​ตัดสิน​ใจ​ว่า​จะ​ครอบคลุม​ทุก interaction กับ​ระบบ​เก่า​หรือ​แค่​บาง​ส่วน และ​ที่​สำคัญ​คือ​ต้อง​ตัดสิน​ใจ​แต่​แรกว่า ACL นี้​เป็น ของ​ถาวร (เมื่อ​ระบบ​ภายนอก​อยู่​นอกเหนือ​การ​ควบคุม​ตลอด​ไป เช่น SaaS ของ​ผู้​ให้​บริการ) หรือ​เป็น นั่งร้าน​ชั่วคราว ที่​จะ​ถูก​รื้อ​ทิ้ง​เมื่อ migration เสร็จ​สมบูรณ์ ถ้า model สอง​ฝั่ง​ไม่มี​ความ​ต่าง​เชิง​ความหมาย​จริง ๆ ก็​ไม่​ควร​ใส่ business rule หรือ orchestration ลงใน ACL เพราะ​จะ​กลาย​เป็น​ภาระ​เกิน​จำเป็น

สมมติ​ระบบ e-commerce ใหม่​ของ​เรา​ต้อง​เรียก​ระบบ inventory แบบ legacy ซึ่ง​ใช้ field ชื่อ qty_onhand, whse_cd และ​คืน​สถานะ​เป็น​รหัส​ตัวเลข (0, 1, 2) ที่​ไม่มี​ความหมาย​ใน​ตัวเอง domain ของ​เรา​ต้องการ​แนวคิด StockLevel และ AvailabilityStatus ที่​ชัดเจน​แทน

// model domain ของเรา — สะอาด ไม่รู้จักรายละเอียดของระบบ legacy
public sealed record StockLevel(string Sku, int QuantityAvailable, AvailabilityStatus Status);
public enum AvailabilityStatus
{
InStock,
LowStock,
OutOfStock
}
// port ที่ domain ต้องการ (domain ไม่รู้ว่าใครมาช่วยตอบ)
public interface IInventoryLookup
{
Task<StockLevel> GetStockLevelAsync(string sku);
}
// DTO ดิบที่ระบบ legacy คืนมา — เต็มไปด้วยศัพท์และรหัสของระบบเก่า
public sealed class LegacyInventoryDto
{
public string WhseCd { get; set; } = string.Empty;
public int QtyOnHand { get; set; }
public int StatusCode { get; set; } // 0=ปกติ, 1=ใกล้หมด, 2=หมด
}
// Anti-Corruption Layer: adapter + translator รวมอยู่ในที่เดียว
public sealed class LegacyInventoryAcl : IInventoryLookup
{
private readonly ILegacyInventoryClient _legacyClient; // เรียก SOAP/DB เดิม
public LegacyInventoryAcl(ILegacyInventoryClient legacyClient)
{
_legacyClient = legacyClient;
}
public async Task<StockLevel> GetStockLevelAsync(string sku)
{
LegacyInventoryDto raw = await _legacyClient.FetchBySkuAsync(sku);
// แปลความหมาย ไม่ใช่แค่แปลงชื่อ field
AvailabilityStatus status = raw.StatusCode switch
{
0 => AvailabilityStatus.InStock,
1 => AvailabilityStatus.LowStock,
2 => AvailabilityStatus.OutOfStock,
_ => throw new InvalidOperationException($"รหัสสถานะที่ไม่รู้จักจากระบบ legacy: {raw.StatusCode}")
};
return new StockLevel(sku, raw.QtyOnHand, status);
}
}

code ฝั่ง domain (application service หรือ aggregate) เห็น​เพียง IInventoryLookup และ StockLevel เท่านั้น ไม่มี​ร่องรอย​ของ WhseCd หรือ​รหัส​ตัวเลข​ปริศนา​ใด ๆ รั่ว​เข้า​มา ถ้า​วัน​หนึ่ง​บริษัท​เปลี่ยน​ไป​ใช้​ระบบ inventory ใหม่​ทั้งหมด สิ่ง​ที่​ต้อง​เขียน​ใหม่​คือ LegacyInventoryAcl เพียง​จุด​เดียว domain ไม่​ต้อง​แตะ​เลย

graph LR
    Domain[Domain Model ของเรา] --> Port[IInventoryLookup]
    Port --> ACL[Anti-Corruption Layer]
    ACL --> Translator[Translator]
    ACL --> Adapter[Adapter or Facade]
    Adapter --> Legacy[Legacy or External System]
    Translator --> Legacy

ACL ไม่ใช่ pattern เดี่ยว ๆ แต่​เป็นการ​ประกอบ pattern เชิง​โครงสร้าง​ที่​รู้จัก​กัน​ดี​เข้า​ด้วย​กัน โดย​เฉพาะ Adapter pattern (GoF) ที่​ทำ​หน้าที่​แปลง interface หนึ่ง​ให้​เข้า​กับ​อีก interface หนึ่ง​ซึ่ง​เป็น​กลไก​หลัก​ที่ ACL ใช้​ใน​ระดับ code และ​มัก​ผสม​กับ Facade เพื่อ​รวบยอด interaction ที่​ซับซ้อน​ของ​ระบบ​ภายนอก​ให้​ดู​เรียบ​ง่าย​จาก​มุมมอง domain

ใน​ระดับ strategic design ของ DDD, ACL คือ​หนึ่ง​ใน​รูปแบบ​ความ​สัมพันธ์​บน Context Map ควบคู่​กับ Conformist, Shared Kernel, Customer-Supplier, และ Open Host Service — เลือก​ใช้​เมื่อ​ทีม​ต้องการ​อิสระ​ทาง model สูงสุด​จาก​ฝั่ง upstream แม้​ต้นทุน​การ​แปล​จะ​สูง​กว่า Conformist ก็ตาม แนวคิด​นี้​ยัง​สอดคล้อง​กับ​หลัก Persistence Ignorance และ Dependency Inversion เพราะ domain จะ​พึ่งพา abstraction (port) ที่​ตนเอง​นิยาม ไม่ใช่​ผูก​ติด​กับ​รายละเอียด​ของ dependency ภายนอก​โดยตรง และ​เป็น​ตัวอย่าง​รูปธรรม​ของ Defensive Programming ที่​ระดับ​สถาปัตยกรรม คือ​การ “ไม่​ไว้ใจ” ข้อมูล/สัญญา​จาก​ภายนอก​จนกว่า​จะ​ผ่าน​การ​ตรวจสอบ​และ​แปล​ความหมาย​แล้ว