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 ย่อย:
- วาง façade คร่อมหน้าระบบเดิม — ช่วงแรก façade ส่งทุกคำขอไปที่ legacy ทั้งหมด (พฤติกรรมเดิมไม่เปลี่ยน) client ไม่รู้สึกถึงความแตกต่างใด ๆ
- เลือก feature/seam ที่จะย้าย — มองหาขอบเขตที่ตัดแยกออกจากส่วนอื่นได้ชัดเจน (ideally สอดคล้องกับ Bounded Context)
- สร้างระบบใหม่สำหรับ feature นั้น — implement ให้ครบ ทดสอบคู่ขนานกับพฤติกรรมเดิม
- สลับ route — เพิ่ม feature นั้นเข้า routing table เพื่อให้ façade เริ่มส่งคำขอที่เกี่ยวข้องไประบบใหม่แทน
- สังเกตและ rollback ได้ทันที — ถ้าพบปัญหา แค่เอา feature ออกจาก routing table ก็กลับไปใช้ legacy ได้โดยไม่ต้อง deploy ใหม่
- ทำซ้ำ จน 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
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”// รายการ 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 ได้จริง) |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Anti-Corruption Layer
- Bounded Context
- Refactoring
- Architectural Agility
- YOLO Architecture
- Big Ball of Mud