Architecture by Implication
ตัดสินใจ design แบบโดยนัย จนสถาปัตยกรรมคลุมเครือและวุ่นวาย
ปัญหาคืออะไร
หัวข้อที่มีชื่อว่า “ปัญหาคืออะไร”Architecture by Implication (ABI) เกิดเมื่อนักพัฒนา ตัดสินใจ design แบบโดยนัย (implicitly) แทนการทำอย่างชัดเจน (explicitly) ทีมได้รับ requirement มา แล้วก็เริ่มเขียน code ทันทีโดยไม่เคยหยุดคิด — ยิ่งไม่เคยเขียนลงเป็นเอกสาร — ว่าสถาปัตยกรรมของระบบควรเป็นอย่างไร แต่ละคนจึงถือสถาปัตยกรรมในหัวของตัวเอง แล้วสมมติว่ามันตรงกับที่คนอื่นคิด
Mark Richards ผู้เขียน Software Architecture Patterns, Antipatterns, and Pitfalls อธิบายว่านี่คือ “การขาด spec สถาปัตยกรรมสำหรับระบบที่กำลังพัฒนา” ในหลายกรณีสถาปัตยกรรมถูก “implied” เอาเองล้วน ๆ และผลลัพธ์มักจบลงด้วยความล้มเหลวที่ร้ายแรง
ABI ต่างจากการไม่มีสถาปัตยกรรมเลยตรงที่ มันดูเหมือนมีอยู่ — code รันได้ feature ส่งมอบได้ แต่ไม่มีใครอธิบายภาพรวมได้ตรงกัน สถาปัตยกรรมจริงจึงเป็นผลรวมของสมมติฐานที่ต่างคนต่างมี ไม่ใช่การตัดสินใจร่วมกัน
ทำไมถึงดูน่าใช้
หัวข้อที่มีชื่อว่า “ทำไมถึงดูน่าใช้”- เร็วกว่าในระยะสั้น — ไม่ต้องประชุมออกแบบ ไม่ต้องรอ sign-off เขียน code ได้ทันทีตั้งแต่วันแรก ถูกใจทีมที่ถูกกดดันเรื่อง deadline
- ทีมมีประสบการณ์ รู้สึกว่า “ทุกคนก็รู้อยู่แล้ว” — สถาปนิกหรือ tech lead ที่เคยสร้างระบบคล้ายกันมาก่อนมักมั่นใจเกินไปว่าความรู้ของตัวเองแพร่กระจายไปถึงทุกคนในทีมโดยอัตโนมัติ ความคุ้นเคยกับบางส่วนของระบบทำให้เกิดความรู้สึกว่า “เรื่องนี้เรารู้กันอยู่แล้ว” ทั้งที่ประสบการณ์และความเข้าใจของแต่ละคนไม่เคยเหมือนกันจริง
- framework ให้โครงสร้างเริ่มต้นมาฟรี ๆ — scaffolding ของ framework (เช่น folder Controllers/Models/Views ที่มากับ MVC template) ทำให้รู้สึกว่า “มีสถาปัตยกรรมอยู่แล้ว” ทั้งที่จริงเป็นเพียงโครง file ไม่ใช่การตัดสินใจเรื่อง boundary, dependency, หรือ data flow
- เอกสารดูเหมือนภาระที่ไม่คืนทุนทันที — การเขียน ADR หรือ diagram ไม่ผลิต feature ที่ส่งมอบได้ในสายตาของผู้บริหารระยะสั้น จึงถูกมองข้ามซ้ำแล้วซ้ำเล่า
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”ปัญหาไม่ได้อยู่ที่ “ไม่มีเอกสาร” แต่อยู่ที่ ทีมไม่ได้ตัดสินใจร่วมกันจริง ๆ ลองนึกภาพเปรียบเทียบง่าย ๆ: ถ้าคนส่วนใหญ่สมมติว่าเครื่องยนต์ Volkswagen Beetle อยู่ด้านหน้ารถ (เหมือนรถทั่วไป) ทั้งที่จริงมันอยู่ด้านหลัง ทุกคนจะแก้ปัญหาหรือซ่อมผิดจุดไปหมด — ทีมที่มีสมมติฐานสถาปัตยกรรมไม่ตรงกันก็ล้มเหลวแบบเดียวกัน เพียงแต่ค้นพบช้ากว่ามาก
- ขอบเขต (boundary) เลือนลาง — ไม่มีใครกำหนดชัดว่า layer ไหนทำหน้าที่อะไร ผลคือ data access logic, business rule, และ UI logic ปนกันไปเรื่อย ๆ จนกลายเป็น Big Ball of Mud
- ความรู้หายไปพร้อมคน — การตัดสินใจสำคัญไม่เคยถูกบันทึก เมื่อคนที่ “รู้” ออกจากทีม บริบทของการตัดสินใจก็หายไปด้วย ทีมใหม่ต้องเดาเอาจาก code หรือแย่กว่านั้นคือกลับไปตัดสินใจซ้ำ (ย้อนกลับการตัดสินใจที่เคยมีเหตุผลดีอยู่แล้วโดยไม่รู้ตัว)
- แต่ละ module ขัดกันเอง — ทีมย่อยที่ทำงานคนละส่วนแต่ไม่เคยยืนยันความเข้าใจร่วมกัน มักออกแบบ contract ระหว่าง module ไม่ตรงกัน ต้องมาแก้ทีหลังด้วยการเขียน adapter หรือ workaround เพิ่ม
- สเกลทีมไม่ได้ — สมมติฐานที่ implicit ทำงานได้พอทนกับทีมเล็ก 2-3 คนที่นั่งใกล้กัน แต่พังทันทีเมื่อทีมโต ขึ้น ทีมกระจายที่ทำงานคนละ time zone หรือมีคนใหม่เข้าร่วม
- แยกไม่ออกว่าอะไรคือการตัดสินใจ อะไรคืออุบัติเหตุ — เมื่อจะ refactor หรือขยายระบบ ทีมไม่รู้ว่าโครงสร้างปัจจุบันเป็นแบบนี้เพราะ “ตั้งใจออกแบบไว้” หรือ “บังเอิญเขียนไปแบบนี้” ทำให้กลัวจะแก้ผิดจุด
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”สมมติทีมกำลังสร้างระบบสั่งซื้อ ไม่มีใครเคยประชุมตกลงเรื่อง layering กันเลย แต่ละคนเข้าใจ “สถาปัตยกรรมแบบ layered” ไม่เหมือนกัน คนหนึ่งเขียน SQL ตรงใน controller เพราะคิดว่าเร็วกว่า อีกคนสร้าง repository แยกใน module อื่นเพราะเคยทำแบบนั้นใน project ก่อน ผลคือระบบมีทั้งสองแนวทางปนกันโดยไม่มีใครตั้งใจ
// AntiPattern: สถาปัตยกรรมแบบ implicit — ไม่มีใครตกลง boundary กันชัดเจนpublic class OrderController{ private readonly SqlConnection _connection;
public OrderController(string connectionString) { // ตัดสินใจ "เข้าถึงฐานข้อมูลตรงจาก controller" แบบไม่มีใครรับรู้หรืออนุมัติ _connection = new SqlConnection(connectionString); }
public void PlaceOrder(int customerId, List<int> productIds) { _connection.Open();
// business rule ปนอยู่กับ data access ปนอยู่กับ presentation var command = new SqlCommand( "INSERT INTO Orders (CustomerId, Status) VALUES (@id, 'Pending')", _connection); command.Parameters.AddWithValue("@id", customerId); command.ExecuteNonQuery();
foreach (var productId in productIds) { // กติกาลด stock ก็ถูกเขียนตรงนี้ ไม่มีใครรู้ว่าควรอยู่ที่ไหน var updateStock = new SqlCommand( "UPDATE Products SET Stock = Stock - 1 WHERE Id = @pid", _connection); updateStock.Parameters.AddWithValue("@pid", productId); updateStock.ExecuteNonQuery(); } }}module อื่นในระบบเดียวกัน กลับใช้ repository pattern ที่คนละคนออกแบบไว้ ทำให้เกิดสองสไตล์ที่ไม่คุยกันในระบบเดียว — นี่คือ ABI ในรูปธรรม: ไม่มีใคร “ตัดสินใจผิด” เป็นรายบุคคล แต่ไม่มีการตัดสินใจร่วมเลยตั้งแต่แรก
ทางแก้คือทำให้ boundary ที่เคย implicit กลายเป็น explicit ผ่าน interface และเอกสารประกอบสั้น ๆ:
// Refactored: boundary ถูกตัดสินใจและบันทึกไว้อย่างชัดเจน// ดู ADR-0007: "Orders ใช้ Repository + Domain Service, ห้าม query ตรงจาก Controller"
public interface IOrderRepository{ void Save(Order order);}
public class OrderService{ private readonly IOrderRepository _orders; private readonly IProductStockService _stock;
public OrderService(IOrderRepository orders, IProductStockService stock) { _orders = orders; _stock = stock; }
public void PlaceOrder(int customerId, IReadOnlyList<int> productIds) { // business rule อยู่ในที่เดียวที่ตกลงกันไว้ ไม่ใช่กระจายตามการเดาของแต่ละคน var order = Order.CreatePending(customerId, productIds); _stock.ReserveFor(productIds); _orders.Save(order); }}
public class OrderController{ private readonly OrderService _service;
public OrderController(OrderService service) => _service = service;
public void PlaceOrder(int customerId, List<int> productIds) => _service.PlaceOrder(customerId, productIds);}Controller ไม่รู้จัก SqlConnection อีกต่อไป layer แต่ละชั้นมีหน้าที่ที่ เขียนไว้เป็นลายลักษณ์อักษร ว่าใครทำอะไร ทีมใหม่ที่เข้ามาไม่ต้องเดาจาก code อย่างเดียว แต่มี ADR อธิบายเหตุผลของ boundary นี้ไว้ด้วย
ทางแก้และการ refactor
หัวข้อที่มีชื่อว่า “ทางแก้และการ refactor”- สร้าง Common Architectural Vision ตั้งแต่ต้น — ทีมทั้งหมดต้องเห็นภาพเดียวกันก่อนเขียน code บรรทัดแรก ไม่จำเป็นต้องเป็นเอกสารหนา ขอแค่ทุกคนตอบคำถามเดียวกันได้ตรงกัน เช่น “layer ไหนคุยกับฐานข้อมูลได้”
- เขียน ADR (Architecture Decision Record) สำหรับการตัดสินใจสำคัญ — ตาม Laws of Software Architecture ทุกการตัดสินใจสถาปัตยกรรมควรมี trade-off ที่อธิบายได้ ADR แบบของ Michael Nygard เก็บแค่ context, decision, status, consequences หนึ่งถึงสองหน้า วางไว้ใน version control คู่กับ code อัปเดตง่ายกว่าเอกสาร spec ก้อนใหญ่ที่ไม่มีใครอ่าน
- สื่อสารและยืนยันความเข้าใจตรงกันจริง — อย่าสมมติว่า “พูดในที่ประชุมแล้วทุกคนเข้าใจเหมือนกัน” ให้ทำ diagram ง่าย ๆ (เช่น C4 model ระดับ container) แล้วให้ทีมช่วยกันตรวจทาน ไม่ใช่แค่ประกาศฝ่ายเดียว
- Update the Plan เมื่อสถาปัตยกรรมเปลี่ยนจริง — เอกสารที่ล้าสมัยยิ่งอันตรายกว่าไม่มีเอกสาร เพราะทำให้คนหลงเชื่อสิ่งที่ไม่จริงอีกต่อไป
- อย่า overcorrect เป็น Big Design Up Front — เป้าหมายคือมีสถาปัตยกรรมที่ “explicit พอ” ให้ทีมตัดสินใจตรงกันได้ ไม่ใช่พยายามออกแบบทุกรายละเอียดล่วงหน้าจนไม่ทันเริ่มเขียน code
flowchart TD
Req[รับ Requirement] --> Decide{ตัดสินใจสถาปัตยกรรมชัดเจนไหม}
Decide -->|ไม่ เขียน code ทันที| Assume[แต่ละคนสมมติ design เอง]
Assume --> Clash[Boundary และ contract ขัดกัน]
Clash --> Mud[กลายเป็น Big Ball of Mud]
Decide -->|ใช่ ตกลงและบันทึก| Vision[Common Architectural Vision]
Vision --> Adr[เขียน ADR บันทึกเหตุผล]
Adr --> Clear[Boundary ชัดเจน ทีมสเกลได้]
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Common Architectural Vision
- Laws of Software Architecture
- Big Ball of Mud
- Assumption-Driven Development
- Update the Plan
- Big Design Up Front
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/antipatterns/architecture-by-implication
- Architecture By Implication - An Easy Anti-Pattern to Fall Into (develpreneur.com)
- Architecture By Implication - An Assumption Anti-Pattern (develpreneur.com)
- Lesson 132 - Architecture by Implication AntiPattern, Mark Richards (developertoarchitect.com)
- Documenting Architecture Decisions, Michael Nygard (cognitect.com)