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

Big Design Up Front

ออกแบบ​ละเอียด​ครบ​ทั้ง​ระบบ​ก่อน​เริ่ม​เขียน​จริง จน​แข็ง​ทื่อ ปรับ​ตัว​ไม่​ได้

Big Design Up Front (BDUF) คือ​แนวทาง​ที่​ทีม​พยายาม ออกแบบ​ระบบ​ทั้งหมด​ให้​ครบถ้วน​และ​ละเอียด​ที่สุด​ก่อน​เริ่ม​เขียน code แม้แต่​บรรทัด​เดียว — วาด class diagram ทุก class, เขียน interface ทุก​ตัว, กำหนด schema ฐาน​ข้อมูล​ทุก​ตาราง, ตัดสิน​ใจ​เรื่อง technology stack ทั้งหมด ก่อน​ที่​จะ​รู้​ด้วย​ซ้ำ​ว่า code ตัว​แรก​จะ​รัน​ได้​จริง​หรือ​ไม่ ชื่อ​สัมพันธ์​กัน​มี​อีก​สอง​คำ​คือ Big Modeling Up Front (BMUF) และ Big Requirements Up Front (BRUF) ซึ่ง Scott Ambler ใช้​อธิบาย​ปรากฏการณ์​เดียวกัน​ใน​บริบท​ของ modeling และ requirements ตาม​ลำดับ

BDUF มัก​เป็น​ผล​พวง​ตาม​ธรรมชาติ​ของ model Waterfall ที่​บังคับ​ให้​ทุก​อย่าง​ไหล​ผ่าน​ขั้นตอน requirements → design → implementation อย่าง​เป็น​เส้นตรง ขั้น design ต้อง “ปิด” ให้​สมบูรณ์​ก่อน​จะ​อนุญาต​ให้​ทีม​พัฒนา​ลงมือ​เขียน code

สิ่ง​สำคัญ​ที่​ต้อง​เข้าใจ​คือ ตรง​ข้าม​ของ BDUF ไม่ใช่ “ไม่มี design เลย” การ​มี​วิสัยทัศน์​รวม (Know Where You Are Going) และ​แผน​ระดับ​สูง​ยัง​จำเป็นอยู่ เพียง​แต่​รายละเอียด​ควร​ค่อย ๆ งอก​ขึ้น​มาระหว่าง​ทาง สอดคล้อง​กับ Gall’s Law ที่​ว่า​ระบบ​ซับซ้อน​ที่​ใช้งาน​ได้​จริง มัก​วิวัฒน์​มา​จาก​ระบบ​ง่าย ๆ ที่​ใช้งาน​ได้​ก่อน ไม่ใช่​ถูก​ออกแบบ​ให้​ซับซ้อน​ตั้งแต่​ต้น

  • สัญชาตญาณ​เรื่อง​ต้นทุน​ของ bug — มี​ความ​เชื่อ​ฝัง​ลึก​ว่าการ​แก้ requirement หรือ design ที่​ผิดพลาด​ตั้งแต่​ขั้น​ออกแบบ ถูก​กว่า​การ​แก้​หลัง code ถูก​เขียน​ไป​แล้ว​มาก จึง​ดู​สม​เหตุ​สม​ผล​ที่​จะ “คิด​ให้​ครบ​ก่อน” เพื่อ​ลด​ความ​เสี่ยง​นั้น
  • ความ​รู้สึก​ควบคุม​ได้ — เอกสาร design ที่​หนา​และ​ครบถ้วน​ให้​ผู้​บริหาร​และ​ผู้​มี​ส่วน​ได้​ส่วน​เสีย​ความ​รู้สึก​ว่า project “อยู่​ภาย​ใต้​การ​ควบคุม” สามารถ​ประเมิน scope, timeline และ budget ได้​แม่นยำ​ล่วงหน้า
  • สัญญา​และ​งบประมาณ​แบบ​ตายตัว — ใน​สัญญา​แบบ fixed-price หรือ​โครงการ​ภาค​รัฐ ผู้​ว่าจ้าง​มัก​ต้องการ​เอกสาร spec ที่​ตายตัว​เพื่อ​ใช้​ผูกมัด​ขอบเขต​งาน​และ​ป้องกัน​ข้อ​พิพาท​ภายหลัง
  • ทีม​ที่​คุ้น​เคย​กับ waterfall — องค์กร​ที่มา​จาก​พื้นเพ​วิศวกรรม​แบบ​ดั้งเดิม (เช่น​วิศวกรรม​โยธา) มัก​เข้าใจ​ว่า​ซอฟต์แวร์​ควร​ออกแบบ​ให้​เสร็จ​สมบูรณ์​ก่อน​สร้าง เหมือน​พิมพ์เขียว​อาคาร โดย​ลืม​ไป​ว่า​ซอฟต์แวร์​เปลี่ยนแปลง (requirements เปลี่ยน) ได้​ง่าย​กว่า​คอนกรีต​มาก
  1. design ล้าสมัย​ก่อน​สร้าง​เสร็จ — ตาม​ที่ Wikipedia อธิบาย: ความ​ต้องการ​ทาง​ธุรกิจ​มัก​เปลี่ยน​เร็ว​กว่า​ที่ project ขนาด​ใหญ่​จะ​สร้าง​เสร็จ ทำให้ design ที่ “สมบูรณ์​แบบ” ใน​วัน​ที่​เขียน กลาย​เป็น​สิ่ง​ล้าสมัย​ตั้งแต่​ก่อน​ที่ implementation จะ​เสร็จ​ด้วย​ซ้ำ
  2. ผู้​ออกแบบ​ไม่มี​ทาง​เห็น​ปัญหา​ที่แท้​จริง​ล่วงหน้า Martin Fowler ชี้​ใน​บทความ Is Design Dead? ว่า​เป็น​ไป​ไม่​ได้ที่​จะ​คิด​ทะลุ​ทุก​ประเด็น​ที่​ต้อง​เจอ​ตอน​เขียน code จริง​ล่วงหน้า — จะ​มี​สมมติฐาน​ผิดพลาด​เสมอ และ design กระดาษ​ก็​ไม่​เคย​ถูก validate ด้วย​การ​รัน​จริง​จนกว่า​จะ​สาย​เกิน​แก้ไข​ง่าย ๆ
  3. ยิ่ง​ลงทุน​ใน​รายละเอียด​มาก ยิ่ง​ต้านทาน​การ​เปลี่ยนแปลง ตาม​ที่ Scott Ambler อธิบาย​ใน BMUF Anti-Pattern: ยิ่ง​ทีม​ทุ่ม​เวลา​ไป​กับ model ละเอียด​มาก​เท่าไร ก็​ยิ่ง​มี​การ​ตัดสิน​ใจ​ผูก​ติด​กับ​รายละเอียด​นั้น​มาก​เท่านั้น ทำให้​ยาก​ที่​จะ​ปรับ​เปลี่ยน​เมื่อ requirement เปลี่ยน และ​ลด​แรง​จูงใจ​ที่​จะ​ยอมรับ​ความ​เปลี่ยนแปลง​ลง​ไป​อีก
  4. Analysis Paralysis แฝง​อยู่​ใน BDUF เสมอ — เมื่อ​พยายาม​ให้ design “ครบ​และ​ถูกต้อง 100%” ทีม​มัก​ติด​อยู่​ใน​วงจร​ถกเถียง​และ​ปรับ​แก้​เอกสาร​ไม่​จบ​สิ้น ดู​เพิ่ม​ที่ Analysis Paralysis
  5. สิ้น​เปลือง​เมื่อ​ทำ​ผิด​ทาง — ถ้า​สมมติฐาน​ตอน​ต้น​ผิด งาน​ออกแบบ​ทั้งหมด​ที่​ลงทุน​ไป​อาจ​ต้อง​ทิ้ง​เกือบ​ทั้งหมด ต่าง​จาก​แนวทาง​ที่​ทำที​ละ​น้อย​และ​ตรวจสอบ​ด้วย​ของ​จริง​เป็น​ระยะ ๆ ซึ่ง​จำกัด​ความ​เสียหาย​ไว้​ใน​ขอบเขต​เล็ก ๆ
  6. ทำให้​ผู้​ออกแบบ​ห่าง​จาก code — Fowler ยัง​ชี้​ว่า​เมื่อ​คน​ออกแบบ​แยก​ขาด​จาก​การ​เขียน code ทักษะ​และ​ความ​เข้าใจ​เทคโนโลยี​ของ​พวก​เขา​จะ​ล้าหลัง​ไป​เรื่อย ๆ และ​เกิด​ความ​ขัดแย้ง​ทาง​วัฒนธรรม​กับ​ทีม​พัฒนา​ที่​ต้อง​เป็น​คน​ตาม​แก้ design ที่​ใช้​จริง​ไม่​ได้

ตัวอย่าง BDUF: ก่อน​เริ่ม​เขียน code ระบบ order-processing แม้แต่​บรรทัด​เดียว ทีม​ออกแบบ interface ที่​ตายตัว​และ “ครบ​สมบูรณ์” ไว้​ล่วงหน้า​ตาม​ที่​คาด​เดา​ว่า​ทุก use case ใน​อนาคต​จะ​ต้อง​ใช้ พร้อม parameter เผื่อ​ไว้​จำนวน​มาก

// ออกแบบไว้ล่วงหน้าทั้งหมดก่อนรู้ว่า caller จริงต้องการอะไร
public interface IOrderProcessor
{
OrderResult ProcessOrder(
Order order,
bool applyDiscount,
bool useLoyaltyPoints,
bool splitShipment,
bool requiresApproval,
string approverEmail,
int retryCount,
TimeSpan timeout,
ShippingCarrier preferredCarrier,
PaymentGatewayType gatewayOverride,
// ... เผื่อไว้อีก10 parameter สำหรับ "feature ที่อาจจะต้องมีในอนาคต"
bool enableFutureLoyaltyTierV3);
// เอกสาร design หนา 80 หน้าถูก sign-off แล้วก่อนเขียน code จริง
// แต่พอเริ่ม implement จริง พบว่า business เปลี่ยนกติกาส่วนลดไปแล้ว
// และ 6 ใน 10 parameter ไม่เคยถูกใช้เลย
}

ปัญหา: เวลา​ส่วน​ใหญ่​ถูก​ใช้​ไป​กับ​การ​เดา​อนาคต​ที่​ไม่มี​ทาง​แม่นยำ interface ที่ “ครบ” กลับ​ซับซ้อน​เกิน​จำเป็น (ขัด​กับ YAGNI) และ​เมื่อ​ความ​จริง​เปลี่ยน เอกสาร​และ code ที่​ผูก​กับ​มัน​ต้อง​ถูก​รื้อ​ใหม่​จำนวน​มาก

แนวทาง​แก้​ไม่ใช่​การ​ทิ้ง design ไป​เลย แต่​คือ​การ ออกแบบ​พอเพียง​สำหรับ​สิ่ง​ที่​รู้​แน่ชัด แล้ว​ปล่อย​ให้​รายละเอียด​ที่​เหลือ​วิวัฒน์​ไป​พร้อม​กับ feedback จริง แนวคิด​นี้​บาง​ครั้ง​เรียก​ว่า Rough Design Up Front (RDUF) หรือ emergent design — วาง skeleton สถาปัตยกรรม​ระดับ​สูง​ไว้​ก่อน (module ไหน​คุย​กับ module ไหน, boundary หลัก​อยู่​ตรง​ไหน) แล้ว​ปล่อย​ให้ interface ภายใน​และ​รายละเอียด implementation ค่อย ๆ ก่อ​ร่าง​จาก code ที่​รัน​ได้​จริง​และ​ถูก refactor ต่อ​เนื่อง

// เริ่มจาก interface เล็กที่สุดที่ตอบโจทย์ use case ที่รู้แน่ชัดตอนนี้
public interface IOrderProcessor
{
OrderResult ProcessOrder(Order order);
}
// เมื่อความต้องการจริงปรากฏขึ้น (เช่นต้องรองรับส่วนลด)
// ค่อยขยายผ่าน refactoring โดยมี automated test คุ้มกันการเปลี่ยนแปลง
public class OrderProcessor : IOrderProcessor
{
private readonly IDiscountPolicy _discountPolicy;
public OrderProcessor(IDiscountPolicy discountPolicy)
{
_discountPolicy = discountPolicy;
}
public OrderResult ProcessOrder(Order order)
{
var discounted = _discountPolicy.Apply(order);
// เพิ่มพฤติกรรมใหม่ทีละ use case ที่พิสูจน์แล้วว่าจำเป็นจริง
return new OrderResult(discounted);
}
}

หลัก​ปฏิบัติ​ที่​ช่วย​ได้:

  • ทำความ​เข้าใจ​เป้าหมาย​รวม​ก่อน​ลงมือ (Know Where You Are Going) เพื่อ​ให้การ​ตัดสิน​ใจ​แต่ละ​ก้าว​สอดคล้อง​ทิศทาง​เดียวกัน โดย​ไม่​ต้อง​ล็อก​รายละเอียด​ทั้งหมด​ล่วงหน้า
  • สร้าง​ระบบ​ง่าย​ที่​ใช้งาน​ได้​ก่อน แล้ว​วิวัฒน์ ตาม Gall’s Law — ระบบ​ซับซ้อน​ที่​ทำงาน​ได้​ดี มัก​มา​จาก​ระบบ​ง่าย​ที่​ทำงาน​ได้​ก่อน​เสมอ
  • ใช้ automated test และ refactoring เป็น​ตาข่าย​นิรภัย แทน​เอกสาร design ตายตัว เพื่อ​ให้​กล้า​ปรับ​โครงสร้าง​เมื่อ​รู้​ข้อมูล​ใหม่ (ตาม​ที่ Fowler อธิบาย​เรื่อง evolutionary design)
  • อย่า​เผื่อ feature ที่​ยัง​ไม่รู้​ว่า​จะ​เกิด ยึด YAGNI เพิ่ม abstraction เมื่อ​มี use case จริง​มารองรับ​เท่านั้น
  • แบ่ง​ความ​ละเอียด​ของ design ตาม​ความ​เสี่ยง ส่วน​ที่​กระทบ​ยาก​ต่อ​การ​เปลี่ยน​ภายหลัง (เช่น data contract ข้าม​ระบบ) ควร​คิด​ให้​รอบคอบ​กว่า ส่วน​ที่​ปรับ​ทีหลัง​ได้​ง่าย (internal implementation) ปล่อย​ให้ emergent ได้​เต็ม​ที่ — แนวคิด​นี้​สอดคล้อง​กับ Architectural Agility
flowchart LR
    A[Big Design Up Front] --> B[design ตายตัวก่อนเขียน code]
    B --> C[requirement เปลี่ยน]
    C --> D[design ล้าสมัย ต้องรื้อใหม่]

    E[Emergent Design] --> F[design ระดับสูงพอเพียง]
    F --> G[เขียน code ทดสอบ รับ feedback จริง]
    G --> H[refactor ตามข้อมูลใหม่]
    H --> G