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 ของระบบเก่าโดยตรง ก็เท่ากับปล่อยให้ปัญหาคุณภาพของระบบเก่า “ปนเปื้อน” การออกแบบของระบบใหม่
บทบาทใน DDD
หัวข้อที่มีชื่อว่า “บทบาทใน DDD”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 ของเรา — สะอาด ไม่รู้จักรายละเอียดของระบบ legacypublic 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 ที่ระดับสถาปัตยกรรม คือการ “ไม่ไว้ใจ” ข้อมูล/สัญญาจากภายนอกจนกว่าจะผ่านการตรวจสอบและแปลความหมายแล้ว
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Bounded Context
- Context Map
- Shared Kernel
- Adapter Pattern
- Defensive Programming
- Persistence Ignorance
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/domain-driven-design/anti-corruption-layer
- Anti-Corruption Layer Pattern - Azure Architecture Center, Microsoft Learn
- Anti-corruption layer - microservices.io (Chris Richardson)
- Wrapping your business logic with anti-corruption layers - .NET Core, The Reformed Programmer
- Bounded Context - Martin Fowler’s Bliki