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

Strangler Fig

ย้าย​ระบบ​เก่า​ไป​ใหม่​ที​ละ module แทน​การ​รื้อ​ทั้งหมด​ใน​ครั้ง​เดียว

ระบบ legacy ที่​ใช้งาน​มา​นาน​หลาย​ปี​มัก​สะสม​ความ​ซับซ้อน​จน​แตะ​ต้อง​ยาก ทุก​การ​แก้ไข​มี​ความ​เสี่ยง​สูง เทคโนโลยี​ที่​ใช้​อาจ​ล้าสมัย และ​ทีม​กลัว​ที่​จะ​เปลี่ยนแปลง​เพราะ​ไม่รู้​ว่า​อะไร​จะ​พัง​ตาม​มา ทาง​เลือก​ดั้งเดิม​คือ “big bang rewrite” — เขียน​ระบบ​ใหม่​ทั้งหมด​แล้ว​ค่อย​สลับ (cutover) แต่​วิธี​นี้​เสี่ยง​สูง​มาก: ใช้​เวลา​นาน ธุรกิจ​ต้อง​หยุด​รอ requirement เปลี่ยน​ระหว่าง​ทาง และ​การ cutover ครั้ง​เดียว​มัก​พัง​ใน​จุด​ที่​ไม่มี​ใคร​คาด​คิด

Strangler Fig (ตั้ง​ชื่อ​โดย Martin Fowler ในปี 2004 จาก​พฤติกรรม​ของ​ต้นไทร​รัด​คอ​ใน​ป่า​ฝน ซึ่ง​เมล็ด​งอก​บน​กิ่ง​ของ​ต้นไม้​เจ้าบ้าน แล้ว​ค่อย ๆ หยั่ง​ราก​ลง​ดิน​โอบ​รัด​ลำต้น​เดิม จน​สุดท้าย​ต้นไม้​เจ้าบ้าน​ตาย​และ​เหลือ​แต่​ต้นไทร​ยืนต้น​แทนที่) แก้​ปัญหา​นี้​ด้วย​แนวคิด​ตรง​ข้าม: แทนที่​จะ​รื้อ​ทั้งหมด​ครั้ง​เดียว ให้​สร้าง façade (หรือ proxy/router) มา​คั่น​หน้า​ระบบ​เดิม แล้ว​ค่อย ๆ ย้าย​ที​ละ feature ไป​ระบบ​ใหม่​ที​ละ​ส่วน โดยที่ client ยัง​คง​เรียก​ผ่าน endpoint เดิม​เสมอ ไม่รู้ตัว​ว่า​กำลัง​มี​การ​ย้าย​เกิด​ขึ้น​อยู่​เบื้องหลัง ระบบ​เดิม​และ​ระบบ​ใหม่​จึง​อยู่​คู่​กัน​ได้​ใน​ระยะ​เปลี่ยน​ผ่าน (coexist) จน​กระทั่ง feature ทั้งหมด​ถูก​ย้าย​ออก​จน​ระบบ​เก่า “ถูก​รัด​คอ” จน​ไม่​เหลือ requests ให้​บริการ​อีก​ต่อ​ไป และ​สามารถ​ปลดระวาง​ได้​อย่าง​ปลอดภัย

หัวใจ​ของ pattern คือ​การ​ทำให้ ความ​เสี่ยง​กระจาย​เป็น​ก้อน​เล็ก ๆ — แต่ละ​รอบ​ย้าย​เฉพาะ​บาง feature ทดสอบ วัดผล แล้ว​ค่อย​เดิน​หน้า​ต่อ แทนที่​จะ​พนัน​ทั้ง project กับ​การ cutover ครั้ง​เดียว

องค์​ประกอบ​หลัก​มี​สาม​ส่วน: Client ที่​เรียก​ผ่าน endpoint เดียว​โดย​ไม่รู้​ว่า​ใคร​ประมวล​ผล​จริง, Façade/Router ที่​ดัก​คำขอ​ทุก​ตัว​แล้ว​ตัดสิน​ใจ​ว่า​จะ​ส่ง​ต่อ​ไป​ที่​ใด, และ​ระบบ​สอง​ฝั่ง​คือ Legacy System (ของ​เดิม) กับ New System (ของ​ใหม่​ที่​ค่อย ๆ เติบโต) โดย façade มัก​เก็บ​รายการ feature ที่​ย้าย​เสร็จ​แล้ว​ไว้​เป็น routing table หรือ feature flag

classDiagram
    class Client {
      +CallApi()
    }
    class StranglerFacade {
      -RoutingTable migratedFeatures
      +Route(Request r)
    }
    class LegacySystem {
      +Handle(Request r)
    }
    class NewSystem {
      +Handle(Request r)
    }
    class RoutingTable {
      +Contains(feature) bool
    }
    Client --> StranglerFacade
    StranglerFacade o-- RoutingTable
    StranglerFacade --> LegacySystem
    StranglerFacade --> NewSystem

จุด​สำคัญ​คือ StranglerFacade เป็น​จุด​เดียว​ที่ client รู้จัก ส่วน LegacySystem และ NewSystem ไม่​จำเป็น​ต้อง​รู้จัก​กัน​โดยตรง (ถ้า​จำเป็น​ต้อง​เรียก​ข้าม​กัน​ระหว่าง​ช่วง​เปลี่ยน​ผ่าน มัก​ใส่ Anti-Corruption Layer คั่น​อีก​ชั้น​เพื่อ​กัน​ไม่​ให้ concept ของ​ระบบ​เก่า​ไหล​เข้าไป​ปน​เปื้อน model ใหม่)

การ​อพยพ​ระบบ​ด้วย Strangler Fig เดิน​เป็น​วงจร​ซ้ำ ๆ ต่อ feature หรือ domain ย่อย:

  1. วาง façade คร่อม​หน้า​ระบบ​เดิม — ช่วง​แรก façade ส่ง​ทุก​คำขอ​ไป​ที่ legacy ทั้งหมด (พฤติกรรม​เดิม​ไม่​เปลี่ยน) client ไม่รู้สึก​ถึง​ความ​แตก​ต่าง​ใด ๆ
  2. เลือก feature/seam ที่​จะ​ย้าย — มอง​หา​ขอบเขต​ที่​ตัด​แยก​ออก​จาก​ส่วน​อื่น​ได้​ชัดเจน (ideally สอดคล้อง​กับ Bounded Context)
  3. สร้าง​ระบบ​ใหม่​สำหรับ feature นั้น — implement ให้​ครบ ทดสอบ​คู่​ขนาน​กับ​พฤติกรรม​เดิม
  4. สลับ route — เพิ่ม feature นั้น​เข้า routing table เพื่อ​ให้ façade เริ่ม​ส่ง​คำขอ​ที่​เกี่ยวข้อง​ไป​ระบบ​ใหม่​แทน
  5. สังเกต​และ rollback ได้​ทันที — ถ้า​พบ​ปัญหา แค่​เอา feature ออก​จาก routing table ก็​กลับ​ไป​ใช้ legacy ได้​โดย​ไม่​ต้อง deploy ใหม่
  6. ทำซ้ำ จน feature ทุก​ตัว​ย้าย​ครบ แล้ว​จึง​ปลดระวาง​ระบบ​เก่า​และ (ถ้า​ต้องการ) ถอด façade ออก​ให้ client คุย​ตรง​กับ​ระบบ​ใหม่
sequenceDiagram
    participant C as Client
    participant F as Facade
    participant L as Legacy
    participant N as New

    C->>F: Request feature X
    F->>F: Check routing table
    alt feature X migrated
        F->>N: Forward
        N-->>F: Response
    else feature X not migrated
        F->>L: Forward
        L-->>F: Response
    end
    F-->>C: Response
// รายการ feature ที่ย้ายไปยังระบบใหม่แล้ว มักเก็บใน config หรือ feature-flag service
// เพื่อสลับ route ได้โดยไม่ต้อง deploy code ใหม่
public interface IMigrationRegistry
{
bool IsMigrated(string featureName);
}
public class InMemoryMigrationRegistry : IMigrationRegistry
{
private readonly HashSet<string> _migrated;
public InMemoryMigrationRegistry(IEnumerable<string> migratedFeatures)
=> _migrated = new HashSet<string>(migratedFeatures, StringComparer.OrdinalIgnoreCase);
public bool IsMigrated(string featureName) => _migrated.Contains(featureName);
}
// Façade/Router: จุดเดียวที่ client เรียก ไม่ว่า feature จะอยู่ระบบไหน
public class OrderStranglerFacade
{
private readonly IMigrationRegistry _registry;
private readonly ILegacyOrderService _legacy; // ระบบเดิม (เช่น SOAP/WebForms)
private readonly INewOrderService _newSystem; // ระบบใหม่ (เช่น microservice)
public OrderStranglerFacade(
IMigrationRegistry registry,
ILegacyOrderService legacy,
INewOrderService newSystem)
{
_registry = registry;
_legacy = legacy;
_newSystem = newSystem;
}
public async Task<OrderResult> GetOrderAsync(string orderId)
{
// "get-order" ย้ายไประบบใหม่แล้ว → ส่งไป new system
if (_registry.IsMigrated("get-order"))
return await _newSystem.GetOrderAsync(orderId);
// ยังไม่ย้าย → คงพฤติกรรมเดิมผ่าน legacy
return await _legacy.GetOrderAsync(orderId);
}
public async Task<OrderResult> CancelOrderAsync(string orderId)
{
// feature ยกเลิกคำสั่งซื้อยังไม่ถูกย้าย ในระหว่างนี้ยิงไป legacy เสมอ
if (_registry.IsMigrated("cancel-order"))
return await _newSystem.CancelOrderAsync(orderId);
return await _legacy.CancelOrderAsync(orderId);
}
}

การ rollback ทำได้​เพียง​ถอด key ออก​จาก IMigrationRegistry — ไม่​ต้อง redeploy หรือ revert commit ใด ๆ ซึ่ง​เป็น​เหตุผล​สำคัญ​ที่​ทำให้ pattern นี้​ปลอดภัย​กว่า​การ cutover ทั้ง​ระบบ

  • ระบบ legacy มี​ขนาด​ใหญ่ ซับซ้อน และ​ธุรกิจ​ยัง​ต้อง​พึ่งพา​ระบบ​นั้น​ทำงาน​ต่อ​เนื่อง​ระหว่าง​การ​ย้าย
  • ต้องการ​ลด​ความ​เสี่ยง​ของ​การ​เปลี่ยน​สถาปัตยกรรม (เช่น monolith → microservices) หรือ​เปลี่ยน​เทคโนโลยี (เช่น WebForms → Blazor) โดย​ไม่​หยุด​ระบบ
  • ต้องการ​เห็น​ผล​ตอบแทน (ROI) และ​ความ​คืบ​หน้า​อย่าง​ค่อย​เป็น​ค่อย​ไป แทนที่​จะ​รอ​ผลลัพธ์​จน​จบ project ใหญ่
  • คำขอ (request) ที่​เข้า​สู่​ระบบ​เดิม​สามารถ​ดัก​จับ (intercept) ได้ และ​มี​สิทธิ์​แก้ code ฝั่ง legacy เพื่อ​เปิด​ทาง​ให้ redirect ได้​จริง
  • ระบบ​มี​ขนาด​เล็ก​และ​สามารถ replace ทั้งหมด​ได้​ใน​เวลา​สั้น ๆ อยู่​แล้ว — การ​ตั้ง façade และ​ดูแล​สอง​ระบบ​คู่​ขนาน​จะ​ยิ่ง​เพิ่ม​ต้นทุน​โดย​ไม่​จำเป็น (ขัด​กับ KISS)
  • ไม่มี​สิทธิ์​เข้าถึง source code ของ​ระบบ​เดิม หรือ​ไม่​สามารถ​ดัก​คำขอ​ที่​ยิง​เข้า​ระบบ​เก่า​ได้ (เช่น client ผูก​กับ endpoint เดิม​แบบ​แก้​ไม่​ได้)
  • ต้องการ​ปลดระวาง​ระบบ​เดิม​โดย​เร็ว​ที่สุด​แบบ​มี deadline ตายตัว ซึ่ง​งบ​เวลา​ไม่​พอ​สำหรับ​การ​ย้าย​ที​ละ​ส่วน
  • ทีม​ไม่มี​วินัย​พอที่​จะ​ดูแล routing table และ cross-system dependency ระหว่าง​สอง​ระบบ​ให้​สอดคล้อง​กัน​ตลอด​ช่วง​เปลี่ยน​ผ่าน อาจ​นำ​ไป​สู่​สภาพ Big Ball of Mud ที่​ระบบ​ใหม่​พัน​กับ​ระบบ​เก่า​แทนที่​จะ​แทนที่​มัน​ได้​จริง
ด้านรายละเอียด
ข้อดีลด​ความ​เสี่ยง​ด้วย​การ​ย้าย​ที​ละ​ก้อน​เล็ก ๆ แทน big bang rewrite
ข้อดีrollback ทำได้​เร็ว เพียง​ปรับ routing table โดย​ไม่​ต้อง deploy ใหม่
ข้อดีเห็น​ความ​คืบ​หน้า​และ ROI ได้​ระหว่าง​ทาง ไม่​ต้อง​รอ​จบ​ทั้ง project
ข้อดีระบบ​เดิม​ยัง​ทำงาน​ให้​บริการ​ได้​ตลอด​ช่วง​เปลี่ยน​ผ่าน ธุรกิจ​ไม่​หยุด​ชะงัก
ข้อ​เสียต้อง​ดูแล​สอง​ระบบ​พร้อม​กัน​ชั่วคราว เพิ่ม​ความ​ซับซ้อน​ของ​โครงสร้าง​พื้นฐาน​และ​การ deploy
ข้อ​เสียfaçade อาจ​กลาย​เป็น single point of failure หรือ​คอ​ขวด​ด้าน performance ถ้า​ออกแบบ​ไม่​ดี
ข้อ​เสียข้อมูล​ที่​ทั้ง​สอง​ระบบ​ใช้​ร่วม​กัน (shared database) ต้อง sync กัน​ให้​ถูกต้อง​ตลอด​การ​ย้าย ซึ่ง​ทำได้​ยาก
ข้อ​เสียใช้​เวลา​นาน​กว่า​การ rewrite ครั้ง​เดียว (ถ้า​ระบบ​เล็ก​พอที่​จะ rewrite ได้​จริง)