Frankencode
code ที่ไม่เคยถูกออกแบบให้ทำงานร่วมกัน ถูกยัดรวมด้วยเทปกาว
ปัญหาคืออะไร
หัวข้อที่มีชื่อว่า “ปัญหาคืออะไร”Frankencode หมายถึง code ที่ ไม่เคยถูกออกแบบให้ทำงานร่วมกัน แต่ถูกดึงมารวมใน application เดียวและยึดไว้ด้วยเทปกาว ลวด และ Adapter pattern เล็กน้อยที่ปะติดปะต่ออย่างเร่งรีบ ชื่อนี้ยืมมาจากนิยาย Frankenstein ของ Mary Shelley — ร่างที่ประกอบขึ้นจากชิ้นส่วนของศพหลายศพ แต่ละชิ้นแข็งแรงในตัวของมันเอง ทว่าเมื่อเย็บติดกันด้วยความรีบเร่งกลับกลายเป็นสัตว์ประหลาดที่ควบคุมไม่ได้
ที่มาของ Frankencode มีสองทาง ทางแรกคือการรวม library หรือระบบภายนอกที่พัฒนาแยกกันโดยสิ้นเชิง แต่ละฝั่งตั้งอยู่บนสมมติฐาน รูปแบบข้อมูล และแบบแผนการเรียกใช้ที่ไม่ตรงกัน ทางที่สองซึ่งพบบ่อยไม่แพ้กันคือ Frankencode ที่เกิดขึ้น ภายใน project เดียวกันเอง เมื่อทีมขาดวิสัยทัศน์เชิงสถาปัตยกรรม ขาดการรีวิว design และเพิ่ม feature ทีละชิ้นแบบต่างคนต่างทำ จนแต่ละส่วนของระบบราวกับถูกเขียนโดยคนละบริษัทที่ไม่เคยคุยกัน
ทำไมถึงดูน่าใช้
หัวข้อที่มีชื่อว่า “ทำไมถึงดูน่าใช้”- เร็วกว่าในระยะสั้น — การดึง library สำเร็จรูปหรือ code ที่มีอยู่แล้วมาแปะ ดูเหมือนประหยัดเวลากว่าการออกแบบ interface กลางให้เรียบร้อยตั้งแต่ต้น โดยเฉพาะภายใต้ deadline แบบ fixed-bid ที่ต้องส่งมอบ feature เป็นรอบ ๆ
- เลี่ยงการ “reinvent the wheel” — ทีมมักถูกสอนให้ไม่เขียนสิ่งที่มีอยู่แล้วซ้ำ (ดู Reinventing the Wheel) จึงหยิบของสำเร็จรูปมาต่อโดยไม่ทันคิดเรื่องความเข้ากันได้
- ไม่มีใครรับผิดชอบภาพรวม — เมื่อแต่ละ feature ถูกมอบหมายให้คนละคนโดยไม่มีการรีวิวสถาปัตยกรรมร่วมกัน ทุกคนต่างคิดว่า code ของตัวเอง “ทำงานได้” — และมันก็ทำงานได้จริง เพียงแค่ทำงานร่วมกับส่วนอื่นไม่ได้อย่างสวยงาม
- การ reuse ฟังดูเป็นคุณธรรมเสมอ — แม้ reuse และ composability จะเป็นเป้าหมายที่ดี แต่คำว่า “reuse ได้” กับ “reuse ได้อย่างเหมาะสม” ไม่ใช่เรื่องเดียวกัน
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”ซอฟต์แวร์ที่พัฒนาแยกกันมักตั้งอยู่บนสมมติฐานและการตัดสินใจ design ที่เข้ากันไม่ได้ — รูปแบบ error handling ไม่ตรงกัน (exception กับ error code), รูปแบบข้อมูลไม่ตรงกัน (XML กับ JSON), model concurrency ไม่ตรงกัน (synchronous กับ asynchronous) เมื่อรวมเข้าด้วยกันโดยไม่มีชั้นแปลงกลาง ผลลัพธ์คือระบบที่งุ่มง่ามและออกแบบไม่ดีเมื่อเทียบกับซอฟต์แวร์ที่ถูกออกแบบมาให้ทำงานร่วมกันใน domain เดียวกันตั้งแต่ต้น
อาการที่พบบ่อยได้แก่
- Cyclomatic complexity สูงผิดปกติ — เต็มไปด้วยเงื่อนไขพิเศษเพื่อรองรับความไม่ตรงกันระหว่างส่วนต่าง ๆ
- ประสิทธิภาพต่ำ — ข้อมูลถูกแปลงไปมาหลายรอบระหว่างชั้นกาว (glue layer) ที่ไม่จำเป็น
- บำรุงรักษายาก — โครงสร้างไม่เป็นระเบียบ แต่ละ feature ดูโดดเดี่ยวแทนที่จะเป็นเนื้อเดียวกัน (cohesive) กับส่วนอื่น
- การเปลี่ยนแปลงหนึ่งจุดกระทบทั้งระบบ — เพราะไม่มีขอบเขตชัดเจนว่าใครพึ่งพาใคร (คล้ายกับ Big Ball of Mud ในระดับสถาปัตยกรรม)
- test ยาก — เพราะ dependency ของแต่ละชิ้นส่วนถูกฝังลึกและปนกันจนแยก unit ไม่ออก
Foote และ Yoder ผู้บัญญัติศัพท์ Big Ball of Mud อธิบายระบบลักษณะนี้ไว้ตรงกันว่าเป็น “haphazardly structured, sprawling, sloppy, duct-tape-and-baling-wire, spaghetti-code jungle” — คำอธิบายเดียวกับที่ใช้กับ Frankencode ทุกประการ เพียงแต่ Frankencode เน้นที่จุดกำเนิดจากการรวมชิ้นส่วนที่ไม่ได้ออกแบบมาด้วยกัน ในขณะที่ Big Ball of Mud อธิบายผลลัพธ์ปลายทางของระบบทั้งก้อน
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”สมมติทีมต้องเชื่อมระบบคำนวณราคาของบุคคลที่สาม (คืนค่าเป็น int error code แบบเก่า) เข้ากับระบบสต็อกสินค้าภายในที่เพิ่งเขียนใหม่ (คืนค่าเป็น Task แบบ async/await) โดยไม่มีการออกแบบ interface กลาง — เพียงแค่เขียน code เรียกใช้ตรง ๆ แล้วยัดทุกอย่างลงใน class เดียว
// Antipattern: Frankencode — เอา library สองระบบที่ไม่เคยออกแบบมาด้วยกันมาต่อกันตรง ๆpublic class CheckoutService{ private readonly LegacyPricingEngine _legacyPricing; // คืนค่า int error code, synchronous private readonly InventoryClientV2 _inventoryClient; // คืนค่า Task, throw exception
public CheckoutService() { // สร้าง instance ตรงในคอนสตรักเตอร์ — ผูกติดกับ implementation ทั้งสองระบบ _legacyPricing = new LegacyPricingEngine(); _inventoryClient = new InventoryClientV2(); }
public async Task<decimal> CalculateTotalAsync(string sku, int quantity) { // ระบบเก่าใช้ error code แทน exception ต้องเช็คแบบ manual int status = _legacyPricing.TryGetUnitPrice(sku, out double rawPrice); if (status != 0) { // ต้องรู้ magic number ของอีกระบบเพื่อแปลความหมาย error throw new InvalidOperationException($"pricing failed code={status}"); }
// ระบบใหม่ throw exception ตอน sku ไม่มีในสต็อก ต้อง try/catch แยกสไตล์ bool inStock; try { inStock = await _inventoryClient.CheckStockAsync(sku, quantity); } catch (InventoryNotFoundException) { inStock = false; }
if (!inStock) return 0m;
// ต้องแปลง double (ของเก่า) เป็น decimal (ของใหม่) เอง ตรงกลาง method return (decimal)rawPrice * quantity; }}ปัญหา: CheckoutService ต้องรู้รายละเอียดภายในของทั้งสองระบบ — รูปแบบ error, ชนิดข้อมูล, และ model concurrency ถ้าวันหนึ่งต้องเปลี่ยน library ราคา ต้องไล่แก้ code ที่ผสมสไตล์กันทั่วทั้ง method และ test ต้อง mock ทั้งสองระบบพร้อมกันเสมอ
ทางแก้และการ refactor
หัวข้อที่มีชื่อว่า “ทางแก้และการ refactor”แนวทางแก้คือกันความไม่เข้ากันไว้ในชั้นเดียว ด้วยการนิยาม port (interface) ที่ตรงกับภาษา domain ของเราเอง แล้วเขียน Adapter แยกต่างหากสำหรับแต่ละระบบภายนอก — แนวคิดเดียวกับ Anti-Corruption Layer ใน DDD ที่กัน model ของ Bounded Context อื่นไม่ให้รั่วไหลเข้ามาปนกับ model ของเรา
flowchart TB
subgraph Before
Core1[CheckoutService]
Glue1[Inline Glue Code]
LibA1[LegacyPricingEngine]
LibB1[InventoryClientV2]
Core1 --> Glue1
Glue1 --> LibA1
Glue1 --> LibB1
end
subgraph After
Core2[CheckoutService]
PortA[IPricingPort]
PortB[IInventoryPort]
AdapterA[LegacyPricingAdapter]
AdapterB[InventoryV2Adapter]
LibA2[LegacyPricingEngine]
LibB2[InventoryClientV2]
Core2 --> PortA
Core2 --> PortB
PortA --> AdapterA
PortB --> AdapterB
AdapterA --> LibA2
AdapterB --> LibB2
end
// ทางแก้: นิยาม port ตามภาษา domain ของเราเองpublic interface IPricingPort{ Task<decimal> GetUnitPriceAsync(string sku);}
public interface IInventoryPort{ Task<bool> IsInStockAsync(string sku, int quantity);}
// Adapter กันความประหลาดของ LegacyPricingEngine ไว้ในที่เดียวpublic class LegacyPricingAdapter : IPricingPort{ private readonly LegacyPricingEngine _legacyPricing;
public LegacyPricingAdapter(LegacyPricingEngine legacyPricing) => _legacyPricing = legacyPricing;
public Task<decimal> GetUnitPriceAsync(string sku) { int status = _legacyPricing.TryGetUnitPrice(sku, out double rawPrice); if (status != 0) throw new PricingUnavailableException(sku, status);
return Task.FromResult((decimal)rawPrice); }}
// Adapter กันสไตล์ exception-based ของ InventoryClientV2 ไว้ในที่เดียวเช่นกันpublic class InventoryV2Adapter : IInventoryPort{ private readonly InventoryClientV2 _inventoryClient;
public InventoryV2Adapter(InventoryClientV2 inventoryClient) => _inventoryClient = inventoryClient;
public async Task<bool> IsInStockAsync(string sku, int quantity) { try { return await _inventoryClient.CheckStockAsync(sku, quantity); } catch (InventoryNotFoundException) { return false; } }}
// CheckoutService รู้จักแค่ port ของตัวเอง ไม่รู้จักระบบภายนอกเลยpublic class CheckoutService{ private readonly IPricingPort _pricing; private readonly IInventoryPort _inventory;
public CheckoutService(IPricingPort pricing, IInventoryPort inventory) { _pricing = pricing; _inventory = inventory; }
public async Task<decimal> CalculateTotalAsync(string sku, int quantity) { if (!await _inventory.IsInStockAsync(sku, quantity)) return 0m;
decimal unitPrice = await _pricing.GetUnitPriceAsync(sku); return unitPrice * quantity; }}ตอนนี้ CheckoutService พูดภาษา domain ของตัวเองล้วน ๆ (IPricingPort, IInventoryPort) ไม่รู้เรื่อง error code หรือ exception type ของระบบภายนอกเลย ถ้าวันหนึ่งเปลี่ยนไปใช้ pricing engine เจ้าอื่น แก้แค่ adapter ตัวเดียว test CheckoutService ก็ mock แค่2 interface เล็ก ๆ แทนที่จะต้อง mock ทั้ง2 library จริง
หลักการที่ช่วยป้องกัน Frankencode ตั้งแต่ต้นทาง
- วางวิสัยทัศน์เชิงสถาปัตยกรรมก่อนเริ่มรวมระบบ ไม่ใช่ปล่อยให้แต่ละ feature ต่อกันเองแบบ ad-hoc
- รักษาความเป็น loosely coupled และ high cohesion (ดู Separation of Concerns)
- ใช้ Dependency Inversion — ให้ core domain นิยาม interface เอง แล้วให้ adapter ไป implement ตาม ไม่ใช่ให้ core ไปปรับตัวตาม library ภายนอก
- เมื่อจะย้ายออกจากระบบเก่าทีละส่วน ใช้ Strangler Fig แทนการรื้อทั้งก้อนพร้อมกัน
- ตรวจสอบ code ที่ดึงมาจากภายนอกก่อนรวม (ดู Found on the Internet) และมี code review / unit test คุมทุกจุดต่อ
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Adapter
- Anti-Corruption Layer
- Big Ball of Mud
- Witches’ Brew Architecture
- Found on the Internet
- Strangler Fig Application