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 เปลี่ยน) ได้ง่ายกว่าคอนกรีตมาก
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”- design ล้าสมัยก่อนสร้างเสร็จ — ตามที่ Wikipedia อธิบาย: ความต้องการทางธุรกิจมักเปลี่ยนเร็วกว่าที่ project ขนาดใหญ่จะสร้างเสร็จ ทำให้ design ที่ “สมบูรณ์แบบ” ในวันที่เขียน กลายเป็นสิ่งล้าสมัยตั้งแต่ก่อนที่ implementation จะเสร็จด้วยซ้ำ
- ผู้ออกแบบไม่มีทางเห็นปัญหาที่แท้จริงล่วงหน้า Martin Fowler ชี้ในบทความ Is Design Dead? ว่าเป็นไปไม่ได้ที่จะคิดทะลุทุกประเด็นที่ต้องเจอตอนเขียน code จริงล่วงหน้า — จะมีสมมติฐานผิดพลาดเสมอ และ design กระดาษก็ไม่เคยถูก validate ด้วยการรันจริงจนกว่าจะสายเกินแก้ไขง่าย ๆ
- ยิ่งลงทุนในรายละเอียดมาก ยิ่งต้านทานการเปลี่ยนแปลง ตามที่ Scott Ambler อธิบายใน BMUF Anti-Pattern: ยิ่งทีมทุ่มเวลาไปกับ model ละเอียดมากเท่าไร ก็ยิ่งมีการตัดสินใจผูกติดกับรายละเอียดนั้นมากเท่านั้น ทำให้ยากที่จะปรับเปลี่ยนเมื่อ requirement เปลี่ยน และลดแรงจูงใจที่จะยอมรับความเปลี่ยนแปลงลงไปอีก
- Analysis Paralysis แฝงอยู่ใน BDUF เสมอ — เมื่อพยายามให้ design “ครบและถูกต้อง 100%” ทีมมักติดอยู่ในวงจรถกเถียงและปรับแก้เอกสารไม่จบสิ้น ดูเพิ่มที่ Analysis Paralysis
- สิ้นเปลืองเมื่อทำผิดทาง — ถ้าสมมติฐานตอนต้นผิด งานออกแบบทั้งหมดที่ลงทุนไปอาจต้องทิ้งเกือบทั้งหมด ต่างจากแนวทางที่ทำทีละน้อยและตรวจสอบด้วยของจริงเป็นระยะ ๆ ซึ่งจำกัดความเสียหายไว้ในขอบเขตเล็ก ๆ
- ทำให้ผู้ออกแบบห่างจาก 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 ที่ผูกกับมันต้องถูกรื้อใหม่จำนวนมาก
ทางแก้และการ refactor
หัวข้อที่มีชื่อว่า “ทางแก้และการ refactor”แนวทางแก้ไม่ใช่การทิ้ง 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