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 ผันผวนถูกแยกออกไปอยู่หลัง interfacepublic 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