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

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 นี้​ไว้​ด้วย

  1. สร้าง Common Architectural Vision ตั้งแต่​ต้น — ทีม​ทั้งหมด​ต้อง​เห็น​ภาพ​เดียวกัน​ก่อน​เขียน code บรรทัด​แรก ไม่​จำเป็น​ต้อง​เป็น​เอกสาร​หนา ขอ​แค่​ทุก​คน​ตอบ​คำถาม​เดียวกัน​ได้​ตรง​กัน เช่น “layer ไหน​คุย​กับ​ฐาน​ข้อมูล​ได้”
  2. เขียน ADR (Architecture Decision Record) สำหรับ​การ​ตัดสิน​ใจ​สำคัญ — ตาม Laws of Software Architecture ทุก​การ​ตัดสิน​ใจ​สถาปัตยกรรม​ควร​มี trade-off ที่​อธิบาย​ได้ ADR แบบ​ของ Michael Nygard เก็บ​แค่ context, decision, status, consequences หนึ่ง​ถึง​สอง​หน้า วาง​ไว้​ใน version control คู่​กับ code อัปเดต​ง่าย​กว่า​เอกสาร spec ก้อน​ใหญ่​ที่​ไม่มี​ใคร​อ่าน
  3. สื่อสาร​และ​ยืนยัน​ความ​เข้าใจ​ตรง​กัน​จริง — อย่า​สมมติ​ว่า “พูด​ใน​ที่​ประชุม​แล้ว​ทุก​คน​เข้าใจ​เหมือน​กัน” ให้​ทำ diagram ง่าย ๆ (เช่น C4 model ระดับ container) แล้ว​ให้​ทีม​ช่วย​กัน​ตรวจทาน ไม่ใช่​แค่​ประกาศ​ฝ่าย​เดียว
  4. Update the Plan เมื่อ​สถาปัตยกรรม​เปลี่ยน​จริง — เอกสาร​ที่​ล้าสมัย​ยิ่ง​อันตราย​กว่า​ไม่มี​เอกสาร เพราะ​ทำให้​คน​หลง​เชื่อ​สิ่ง​ที่​ไม่​จริง​อีก​ต่อ​ไป
  5. อย่า 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 ชัดเจน ทีมสเกลได้]