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

Principle of Architectural Agility

ออกแบบ​สถาปัตยกรรม​ให้​ปรับ​ตัว​ตาม​ความ​เปลี่ยนแปลง​ได้

Architectural Agility ไม่ใช่ pattern เดียว​หรือ​เทคนิค​เดียว แต่​เป็นหลักการ​เชิง​ปรัชญาที่​บอกว่า สถาปัตยกรรม​ซอฟต์แวร์​ที่​ดี​ต้อง​สามารถ วิวัฒน์ ตาม​ความ​ต้องการ​ทาง​ธุรกิจ เงื่อนไข​การ​ใช้งาน หรือ​เทคโนโลยี​ที่​เปลี่ยน​ไป​ได้ โดย​ไม่​ต้อง​รื้อ​ระบบ​ทั้งหมด หลักการ​นี้​ปรากฏ​ชัดเจน​ที่สุด​ใน​หนังสือ Building Evolutionary Architectures ของ Neal Ford, Rebecca Parsons และ Patrick Kua (O’Reilly, 2017)

หมายเหตุ​ผู้​แปล: ต้นฉบับ DevIQ ให้​เครดิต​หนังสือ​เล่ม​นี้​ว่า​เป็น​ของ “Ford และ Parsons” เท่านั้น แต่​ฉบับ​เต็ม​มี​ผู้​เขียน​ร่วม​สาม​คน​คือ Neal Ford, Rebecca Parsons และ Patrick Kua

หัวใจ​ของ​แนวคิด​นี้​คือ evolutionary architecture — แทนที่​จะ​ออกแบบ​สถาปัตยกรรม​ให้​เสร็จ​สมบูรณ์​ก่อน​เริ่ม​เขียน code (Big Design Up Front) สถาปัตยกรรม​ควร​ถูก​มอง​เป็น​ความ​พยายาม​ต่อ​เนื่อง​ที่​ทำงาน​คู่​ขนาน​ไป​กับ​การ​พัฒนา ตอบ​สนอง​ทั้ง​ความ​ต้องการ​ที่​เปลี่ยนแปลง​และ feedback จาก​การ​เขียน code จริง Martin Fowler อธิบาย​ไว้​ใน​คำนำ​ของ​หนังสือ​เล่ม​นี้​ว่า สถาปัตยกรรม​ที่​ดี “สำคัญ​เกิน​กว่า​จะ​ปล่อย​ให้​เกิด​ขึ้น​โดย​บังเอิญ” — agility ไม่​ได้​แปล​ว่า​ไม่​วางแผน แต่​แปล​ว่า​วางแผน​อย่าง​ตั้งใจ​ให้ เปลี่ยน​ทิศ​ได้ เมื่อ​จำเป็น

กลไก​สำคัญ​ที่​ทำให้ evolutionary architecture เป็น​จริง​ได้​คือ:

  • Fitness functions — ชุด​การ​ทดสอบ​ที่​วัดผล​ได้ (เช่น performance test, security scan, coupling metric) ที่​คอย​ตรวจสอบ​ว่า​สถาปัตยกรรม​ยัง​คง “เหมาะสม” อยู่​หรือ​ไม่​ทุก​ครั้ง​ที่​มี​การ​เปลี่ยนแปลง ทำ​หน้าที่​เป็น​ราว​กัน​ตก​ไม่​ให้​คุณสมบัติ​สำคัญ​เสื่อมถอย​ไป​เมื่อ​ระบบ​วิวัฒน์
  • Incremental change — เปลี่ยนแปลง​ที​ละ​น้อย พร้อม feedback loop สั้น ๆ แทน​การ “รื้อ​ใหญ่” เป็น​ครั้ง​คราว
  • Continuous Delivery — เป็น​ปัจจัย​เปิด​ทาง​สำคัญ​ที่​ทำให้ evolutionary architecture ใช้งาน​ได้​จริง​ใน​ทาง​ปฏิบัติ เพราะ​ทำให้การ​เปลี่ยนแปลง​เล็ก ๆ ถูก validate ใน​สภาพ​แวดล้อม​จริง​ได้​บ่อย​และ​ปลอดภัย

สิ่ง​ที่ DevIQ ระบุ​ว่า​สนับสนุน architectural agility ได้แก่:

  • Agile Software Development — ระเบียบ​วิธี​ที่​เน้น​การ​พัฒนา​แบบวน​ซ้ำ (iterative) และ​ปรับ​ตัว​ตาม​การ​เปลี่ยนแปลง ซึ่ง​มัก​เปิด​ทาง​ให้​เกิด architectural agility อย่าง​ต่อ​เนื่อง
  • Microservices Architecture — ออกแบบ​มา​เพื่อ​แยก​ระบบ​ออก​เป็น​บริการ​เล็ก ๆ ที่ loosely coupled ซึ่ง​พัฒนา deploy และ scale ได้​อย่าง​อิสระ แต่ละ microservice วิวัฒน์​ได้​โดย​ไม่​กระทบ​ทั้ง​ระบบ (แต่​ถ้า​เลือก​ใช้​เพราะ​กระแส​มากกว่า​เหตุผล​ด้าน​การ​แยก​ส่วน อาจ​ได้ “distributed big ball of mud” แทน)
  • Continuous Delivery และ DevOps — แนว​ปฏิบัติ​ที่​เน้น​ความ​ร่วมมือ​ใกล้​ชิด​ระหว่าง​ทีม​พัฒนา​และ​ทีม​ปฏิบัติการ ส่งเสริม​การ​ส่ง​มอบ​ซอฟต์แวร์​ที่​รวดเร็ว บ่อย​ครั้ง และ​เชื่อถือ​ได้

สถาปัตยกรรม​ที่​แข็ง​ทื่อ (rigid architecture) มี​ต้นทุน​ที่​มอง​ไม่​เห็น​ใน​วัน​แรก แต่​จะ​โผล่​มา​ทุก​ครั้ง​ที่​ธุรกิจ​เปลี่ยน​ทิศ เมื่อ​การ​เพิ่ม​ผู้​ให้​บริการ​ชำระ​เงิน​ราย​ใหม่ การ​สลับ​ฐาน​ข้อมูล หรือ​การ​รองรับ​ผู้​ใช้​จำนวน​มาก​ขึ้น กลาย​เป็น project รื้อ​ระบบ​ขนาด​ใหญ่​แทนที่​จะ​เป็นการ​เปลี่ยนแปลง​เฉพาะ​จุด ทีม​จะ​เริ่ม​กลัว​การ​แตะ​ต้อง​สถาปัตยกรรม และ​ความ​กลัว​นั้น​เอง​ที่​ทำให้ debt สะสม​เร็ว​ขึ้น​ไป​อีก

Simon Brown ชี้​ประเด็น​สำคัญ​ว่า สถาปัตยกรรม​กับ agility ไม่​ได้​ขัดแย้ง​กัน — สถาปัตยกรรม​ที่​ดี​ต่างหาก​ที่​ทำให้​ทีม​ตอบ​สนอง​ต่อ​ความ​ต้องการ​ทาง​ธุรกิจ กระบวนการ และ​โครงสร้าง​องค์กร​ที่​เปลี่ยน​ไป​ได้ กุญแจ​สำคัญ​คือ modularity ที่​ดี: เมื่อ codebase ไม่มี​โครงสร้าง​ชัดเจน (เช่น Big Ball of Mud) การ​เปลี่ยนแปลง​เล็ก ๆ ก็​อาจ​ทำให้​ส่วน​ที่​ดู​ไม่​เกี่ยวข้อง​กัน​พัง​ตาม​ไป​ด้วย แต่ Brown ก็​เตือน​ว่า agility ไม่​ได้​มา​ฟรี ๆ ต้อง​ใช้​ความ​ตั้งใจ​ใน​การ​ออกแบบ​และ​ชั่ง​น้ำหนัก trade-off จริง ๆ

บทความ​ของ InfoQ เรื่อง​การ​ถ่วง​ดุล​ระหว่าง Minimum Viable Product (MVP) กับ Minimum Viable Architecture (MVA) เสริม​มุมมอง​นี้​ว่า ไม่​ว่า​จะ​ตั้งใจ​หรือ​ไม่ ทุก​ทีม​จะ​จบ​ลง​ด้วย​สถาปัตยกรรม​แบบ​ใด​แบบ​หนึ่ง​เสมอ คำถาม​คือ​มัน​เกิด​จาก​การ​ตัดสิน​ใจ​อย่าง​มี​สติ​หรือ​เกิด​ขึ้น​เอง แนวทาง​ที่​ช่วย​ให้​เปลี่ยนแปลง​ได้​ง่าย​ใน​อนาคต​คือ:

  • Modularization — code ที่​แบ่ง module ดี​ช่วย​ลด​งาน​ซ้ำ ด้วย​การ​จำกัด​ผลกระทบ​ของ​การ​เปลี่ยนแปลง​ให้​อยู่​ใน​ขอบเขต​เดียว ไม่​กระเพื่อม​ไป​ทั้ง codebase
  • Isolation — แยก dependency ที่​ผูก​กับ​เทคโนโลยี​เฉพาะ​เจาะจง​ออก​มา ทำให้​ทดแทน​ได้​ง่าย​ขึ้น​ภายหลัง
  • Replaceability — ออกแบบ​ให้​ส่วนประกอบ​สลับ​เปลี่ยน​ได้ ลด​ต้นทุน​ของ​การ​ย้อน​กลับ​คำ​ตัดสิน​ใจ

พูด​ใน​เชิง​เทคนิค​ระดับ module นี่​คือ​แนวคิด​เดียว​กับ​ที่ Robert C. Martin อธิบาย​ผ่าน​ทิศทาง​ของ dependency: ส่วน​ที่​ผันผวน (volatile) ควร​พึ่งพา​ส่วน​ที่​มั่นคง (stable) ผ่าน abstraction ไม่ใช่​ใน​ทาง​กลับ​กัน เมื่อ core domain ขึ้น​กับ interface แทนที่​จะ​ขึ้น​กับ implementation รายละเอียด (เช่น SDK ของ​ผู้​ให้​บริการ​เฉพาะ​ราย) การ​เปลี่ยน​ผู้​ให้​บริการ​ก็​ไม่​กระทบ core เลย — นี่​คือ agility ที่​จับ​ต้อง​ได้​ใน​ระดับ code

graph TD
  Core[CoreDomain]
  PaymentPort[IPaymentGateway]
  ShippingPort[IShippingProvider]
  Stripe[StripeGateway]
  PayPal[PayPalGateway]
  Fedex[FedexProvider]
  Core --> PaymentPort
  Core --> ShippingPort
  Stripe -.-> PaymentPort
  PayPal -.-> PaymentPort
  Fedex -.-> ShippingPort

แผนภาพ​นี้​แสดง​ให้​เห็น​ว่า CoreDomain ขึ้น​กับ abstraction (IPaymentGateway, IShippingProvider) ที่​มั่นคง​เท่านั้น ส่วน implementation ที่​ผันผวน เช่น StripeGateway หรือ FedexProvider เป็น​ฝ่าย implement interface เหล่า​นั้น​เข้า​มา ไม่ใช่ core ที่​ต้อง​รู้จัก​รายละเอียด​ของ​แต่ละ​ผู้​ให้​บริการ — จะ​เพิ่ม PayPalGateway ตัว​ใหม่​ก็​ทำได้​โดย​ไม่​ต้อง​แตะ CoreDomain เลย

สมมติ​ระบบ​สั่ง​ซื้อ​สินค้า​ต้อง​เรียก​ใช้​บริการ​ชำระ​เงิน​ภายนอก code ต่อ​ไป​นี้​แสดง​ความ​แตก​ต่าง​ระหว่าง​สถาปัตยกรรม​ที่​แข็ง​ทื่อ​กับ​สถาปัตยกรรม​ที่​ปรับ​ตัว​ได้

// ไม่ดี: OrderService ผูกติดกับ StripeClient โดยตรง
// การเปลี่ยนผู้ให้บริการชำระเงิน หรือเพิ่มผู้ให้บริการรายใหม่
// ต้องแก้ไข OrderService ทุกครั้ง — สถาปัตยกรรมไม่ยืดหยุ่น
public class OrderService
{
private readonly StripeClient _stripeClient = new StripeClient("sk_live_...");
public void CheckoutOrder(Order order)
{
// ธุรกิจหลักถูกฝังปนกับรายละเอียดของ Stripe SDK
var charge = _stripeClient.CreateCharge(order.TotalAmount, order.CustomerCardToken);
if (charge.Status == "succeeded")
{
order.MarkAsPaid();
}
}
}
// ดี: OrderService พึ่งพา abstraction เท่านั้น
// implementation ผันผวนถูกแยกออกไปอยู่หลัง interface
public interface IPaymentGateway
{
PaymentResult Charge(decimal amount, string customerToken);
}
public class StripePaymentGateway : IPaymentGateway
{
private readonly StripeClient _client = new StripeClient("sk_live_...");
public PaymentResult Charge(decimal amount, string customerToken)
{
var charge = _client.CreateCharge(amount, customerToken);
return charge.Status == "succeeded" ? PaymentResult.Success : PaymentResult.Failed;
}
}
public class OrderService
{
private readonly IPaymentGateway _paymentGateway;
// ฉีด dependency เข้ามาผ่าน constructor — core ไม่รู้จัก Stripe เลย
public OrderService(IPaymentGateway paymentGateway)
{
_paymentGateway = paymentGateway;
}
public void CheckoutOrder(Order order)
{
var result = _paymentGateway.Charge(order.TotalAmount, order.CustomerCardToken);
if (result == PaymentResult.Success)
{
order.MarkAsPaid();
}
}
}

ใน version ที่​สอง เมื่อ​ธุรกิจ​ต้องการ​เพิ่ม PayPalPaymentGateway หรือ​สลับ​ผู้​ให้​บริการ​ทั้งหมด​เพราะ​เงื่อนไข​สัญญา​เปลี่ยน ก็​เพียง​เพิ่ม class ใหม่​ที่ implement IPaymentGateway แล้ว​สลับ​ที่​จุด​ประกอบ (composition root) โดย​ไม่​ต้อง​แตะ OrderService หรือ​เขียน unit test ของ business logic ใหม่​เลย — นี่​คือ​ความหมาย​ของ “สถาปัตยกรรม​ที่​ปรับ​ตัว​ได้” ใน​ระดับ​ปฏิบัติ​จริง

  • เปลี่ยน​อะไร​เล็ก ๆ แต่​ต้อง​แก้​หลาย module ที่​ไม่​เกี่ยวข้อง​กัน — อาการ​แบบ shotgun surgery ที่​กระจาย​ไป​ทั่ว codebase บ่ง​บอกว่า​ไม่มี modular boundary ที่​ชัดเจน
  • Business logic ผูก​ติด​กับ​รายละเอียด​ทาง​เทคนิค​ของ​ผู้​ให้​บริการ​หรือ framework เฉพาะ​เจาะจง เช่น เรียก SDK ของ vendor ตรง ๆ กระจาย​อยู่​ทั่ว​ชั้น UI และ domain โดย​ไม่มี abstraction คั่น​กลาง ทำให้​เปลี่ยน vendor แทบ​เป็น​ไป​ไม่​ได้
  • ต้อง deploy ทั้ง​ระบบ​ทุก​ครั้ง​แม้​เปลี่ยน​แค่​จุด​เดียว เพราะ​ทุก​ส่วน​ถูก compile และ deploy รวม​กัน​โดย​ไม่มี​ขอบเขต​ที่​แยก release ได้​อิสระ
  • ไม่มี automated test หรือ fitness function ใด ๆ คอย​ตรวจสอบ ทำให้​ทุก​การ​เปลี่ยนแปลง​สถาปัตยกรรม​กลาย​เป็นการ​พนัน ทีม​จึง​หลีก​เลี่ยง​การ​แตะ​ต้อง​มัน​จน​กลาย​เป็น code แช่แข็ง (frozen caveman)
  • สถาปัตยกรรม​ถูก​ตัดสิน​ใจ​ครั้ง​เดียว​ตอน​ต้น project แล้ว​ไม่​เคย​ทบทวน​อีก (Big Design Up Front) โดย​ไม่มี​กลไก​ใด​คอย​ตรวจสอบ​ว่า​สมมติฐาน​เดิม​ยัง​ใช้ได้​อยู่​หรือ​ไม่​เมื่อ​ความ​ต้องการ​เปลี่ยน​ไป
  • เลือก​สไตล์​สถาปัตยกรรม เช่น microservices เพราะ​กระแส​นิยม ไม่ใช่​เพราะ​ช่วย​แยก​ส่วน​ที่​ผันผวน​ออก​จาก​ส่วน​ที่​มั่นคง​จริง ๆ ผลลัพธ์​มัก​เป็น​ความ​ซับซ้อน​แบบ​กระจาย​ศูนย์ (distributed complexity) โดย​ไม่​ได้ agility เพิ่ม​ขึ้น​เลย
  • Boy Scout Rule — ปรับปรุง code ที​ละ​น้อย​อย่าง​ต่อ​เนื่อง แนว​ปฏิบัติ​ระดับ​จุลภาค​ที่​หนุน evolutionary architecture ใน​ระดับ​มหภาค
  • Separation of Concerns — รากฐาน​ของ modularity ที่​ทำให้​แต่ละ​ส่วน​เปลี่ยนแปลง​ได้​อย่าง​อิสระ
  • Dependency Inversion Principle — กลไก​ระดับ code ที่​ทำให้ core พึ่งพา abstraction แทน​รายละเอียด​ที่​ผันผวน ตาม​ตัวอย่าง​ใน​บทความ​นี้
  • Open/Closed Principle — ขยาย​พฤติกรรม​ได้​โดย​ไม่​ต้อง​แก้ code เดิม หลักการ​ระดับ class ที่​สะท้อน agility ใน​ภาพ​ใหญ่
  • YAGNI — คู่​ตรง​ข้าม​ที่​ถ่วง​ดุล​กัน อย่า​สร้าง​ความ​ยืดหยุ่น​เกิน​ความ​จำเป็น​ก่อน​มี fitness function มา​ยืนยัน
  • Artificial Coupling — code smell ที่​มัก​เป็น​สัญญาณ​เตือน​ล่วงหน้า​ว่า​สถาปัตยกรรม​กำลัง​สูญเสีย agility