Pain Driven Development
อย่าเพิ่งใช้หลักการ จนกว่าจะเริ่ม “เจ็บ”
คืออะไร
หัวข้อที่มีชื่อว่า “คืออะไร”Pain Driven Development (PDD) คือแนวปฏิบัติที่ รอ ที่จะนำหลักการ pattern และแนวปฏิบัติต่าง ๆ มาใช้กับ code จนกว่าแนวทางที่ใช้อยู่ปัจจุบันจะเริ่มสร้างความเจ็บปวด (pain) ที่ต้องแก้ไขจริง ๆ ไม่ใช่เพราะ “ดูดี” หรือ “อาจจะต้องใช้ในอนาคต” มันเป็นรูปแบบหนึ่งของ YAGNI ที่ช่วยประกันว่าคุณจะไม่ทำ gold-plating กับ codebase อย่างสุ่มสี่สุ่มห้าโดยไม่ได้เพิ่มคุณค่าให้ลูกค้า
แนวคิดนี้สอดคล้องกับสิ่งที่ Martin Fowler อธิบายไว้ในบทความเรื่อง Yagni ว่า feature หรือ abstraction ที่สร้างล่วงหน้าเพื่อ “เผื่ออนาคต” มีต้นทุนซ่อนอยู่หลายชั้น ทั้ง cost of build (แรงในการวิเคราะห์ เขียน และทดสอบสิ่งที่ยังไม่จำเป็น) cost of delay (เวลาที่เสียไปแทนที่จะส่งมอบ feature ที่สร้างรายได้จริงก่อน) cost of carry (ความซับซ้อนที่ถ่วงการพัฒนาส่วนอื่น) และ cost of repair (เมื่อ abstraction ที่เดาไว้ผิดทาง ต้องรื้อทิ้งทีหลัง) PDD จึงไม่ใช่ข้ออ้างให้ “ไม่ออกแบบ” แต่คือการเลื่อนการลงทุนออกแบบไปยังจุดที่มีข้อมูลจริงมากพอ นั่นคือ ความเจ็บปวดที่เกิดขึ้นซ้ำ ๆ
ตัวอย่างที่ดี: Open-Closed Principle
หัวข้อที่มีชื่อว่า “ตัวอย่างที่ดี: Open-Closed Principle”ตัวอย่างที่เห็นภาพชัดของการใช้ PDD คือการใช้กับ Open-Closed Principle ไม่มีซอฟต์แวร์ใดที่ “เปิดรับการขยาย แต่ปิดต่อการแก้ไข” ได้ในทุกทางที่เป็นไปได้ หากทำเช่นนั้นจริง มันจะนามธรรมเกินกว่าจะใช้งานได้ — เหมือนกับการส่งมอบ IDE ให้ลูกค้าไปสร้างอะไรก็ได้ที่ต้องการ แทนที่จะส่งมอบซอฟต์แวร์ที่เขาร้องขอจริง ๆ
ดังนั้นแนวทางที่แนะนำสำหรับ OCP คือ ปล่อยให้การเปลี่ยนแปลงหนึ่งหรือสองครั้งแรกต้องแก้ไข code เดิมไปก่อน เขียน conditional เพื่อรองรับพฤติกรรมใหม่ ทำสิ่งที่ง่ายที่สุดที่ทำให้ซอฟต์แวร์รองรับความต้องการใหม่ได้ ต่อเมื่อสิ่งนี้เกิดขึ้นซ้ำแล้วซ้ำเล่า และค่อนข้างชัดเจนแล้วว่ามันจะวิวัฒน์ไปในทิศทางนี้ต่อไป จึงค่อยลงมือใช้ OCP เพื่อป้องกันการเปลี่ยนแปลง ชนิดที่คุณเห็นว่า code ต้องเผชิญจริง ๆ ตามความต้องการที่มาจากลูกค้า
รอให้เห็นรูปแบบก่อน: abstraction ที่ดีเกิดจากการเห็นการเปลี่ยนแปลงจริงเกิดซ้ำ ๆ ไม่ใช่จากการเดาอนาคต การใส่ OCP ก่อนเวลาอันควรมักได้ abstraction ผิดจุด ที่ต้องรื้อทิ้งทีหลังอยู่ดี
ทำอย่างไร
หัวข้อที่มีชื่อว่า “ทำอย่างไร”- ทำสิ่งที่ง่ายที่สุดก่อน — เขียน code ตรงไปตรงมาเพื่อตอบโจทย์ปัจจุบัน ยอมให้มี
if/switchหรือ code ซ้ำเล็กน้อยในช่วงแรก แทนที่จะรีบสร้าง interface หรือ base class เผื่อกรณีในอนาคตที่ยังไม่เกิดขึ้นจริง - สังเกตสัญญาณของ pain — เช่น ต้องแก้ file เดิมซ้ำ ๆ ทุกครั้งที่มีความต้องการใหม่คล้ายเดิม (ใกล้เคียงกับกลิ่น Shotgun Surgery) bug เดิมเกิดซ้ำในหลายจุด ทีมทดสอบหรือลูกค้าบ่นเรื่องเดิมซ้ำ ๆ หรือการเปลี่ยนแปลงเล็กน้อยใช้เวลานานผิดปกติ
- แยกความเจ็บที่รอได้ ออกจากความเจ็บที่ต้องป้องกันล่วงหน้า — pain ส่วนใหญ่ (โครงสร้าง conditional, การจัดกลุ่ม method) แก้ทีหลังได้ด้วย refactor แต่ pain บางประเภทที่ แก้ยากมากเมื่อ project โตแล้ว เช่น การวาง dependency structure หรือ layering ผิดตั้งแต่ต้น ควรใช้ประสบการณ์นำทางและป้องกันไว้ก่อน
- นับจำนวนครั้งที่ pain เกิดซ้ำ — แนวทางทั่วไปคือ “rule of three”: ถ้าเจอ pattern เดิมซ้ำเป็นครั้งที่สามจึงค่อย refactor ไปสู่ abstraction ที่เหมาะสม เพราะตอนนั้นมีตัวอย่างพอที่จะออกแบบ abstraction ที่ตรงกับของจริง ไม่ใช่ของที่เดาไว้
- จัดลำดับความสำคัญของ pain ด้วยผลกระทบเทียบกับแรงที่ต้องลงทุน — ทีมจำนวนมากใช้ตารางสี่ช่อง (effort สูง/ต่ำ × impact สูง/ต่ำ) เพื่อตัดสินใจว่าจะแก้ pain จุดไหนก่อน แทนที่จะไล่แก้ทุกอย่างตามลำดับที่มันโผล่มา
flowchart TD
A[เขียน code แบบง่ายที่สุดที่ตอบโจทย์ตอนนี้] --> B{เจอ pain หรือยัง}
B -- ยังไม่เจอ --> A
B -- เจอแล้ว --> C{เป็น pain ที่แก้ยากภายหลังหรือเกิดซ้ำหลายครั้ง}
C -- เกิดครั้งแรก และแก้ทีหลังได้ --> D[แก้แบบเฉพาะหน้า เช่น เพิ่ม conditional]
D --> A
C -- เกิดซ้ำหรือแก้ยากถ้าปล่อยไว้ --> E[นำหลักการหรือ pattern มาใช้จริง]
E --> A
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”สมมติว่าระบบต้องคำนวณส่วนลดราคาสินค้า ช่วงแรกมีกฎแค่แบบเดียว การเขียนแบบง่ายที่สุดจึงเพียงพอ:
public decimal CalculateDiscount(Order order){ // กฎแรก: ลูกค้าประจำลด 10% if (order.Customer.IsLoyaltyMember) { return order.Total * 0.10m; }
return 0m;}เมื่อความต้องการที่สองมาถึง (ลดราคาช่วงเทศกาล) ทีมยังไม่ต้องรีบสร้าง Strategy pattern — แค่เพิ่ม conditional เข้าไป เพราะยังไม่เห็นรูปแบบที่ชัดเจนพอจะออกแบบ abstraction ได้ถูกจุด:
public decimal CalculateDiscount(Order order){ if (order.IsSeasonalPromotion) { return order.Total * 0.15m; }
if (order.Customer.IsLoyaltyMember) { return order.Total * 0.10m; }
return 0m;}ต่อเมื่อกฎส่วนลดเพิ่มเป็นกฎที่สาม สี่ และมีสัญญาณว่าจะเพิ่มเรื่อย ๆ ตาม business ที่โตขึ้น — นั่นคือ pain ที่เกิดซ้ำจนเห็นรูปแบบชัดแล้ว — จึงค่อยแตกออกเป็น Strategy pattern เพื่อให้แต่ละกฎอยู่แยกกันและเพิ่มกฎใหม่ได้โดยไม่ต้องแก้ method เดิม (สอดคล้องกับ Open-Closed Principle):
public interface IDiscountRule{ bool AppliesTo(Order order); decimal Calculate(Order order);}
public class DiscountCalculator{ private readonly IEnumerable<IDiscountRule> _rules;
public DiscountCalculator(IEnumerable<IDiscountRule> rules) => _rules = rules;
public decimal CalculateDiscount(Order order) => _rules.FirstOrDefault(r => r.AppliesTo(order))?.Calculate(order) ?? 0m;}การ refactor นี้เกิด “ตอนที่เจ็บจริง” ไม่ใช่ตอนเขียนกฎแรก — ทำให้ abstraction ที่ได้ตรงกับกฎส่วนลดจริงสี่ห้าแบบที่มีอยู่ แทนที่จะเป็น interface ที่เดาไว้ล่วงหน้าและใช้ไม่ตรงกับของจริง
ทีมจำนวนมากยังใช้ตาราง effort เทียบ impact เพื่อจัดคิว pain ที่ควรแก้ก่อน:
| Effort ในการแก้ | Impact ต่อทีม/ลูกค้า | แนวทาง |
|---|---|---|
| ต่ำ | สูง | แก้ทันที — คุ้มค่าชัดเจน |
| สูง | สูง | วางแผน refactor เป็นงานถัดไป |
| ต่ำ | ต่ำ | แก้ระหว่างทางถ้ามีเวลา (boy scout rule) |
| สูง | ต่ำ | ปล่อยไว้ก่อน จนกว่า impact จะเพิ่มขึ้นจริง |
ประโยชน์และข้อควรระวัง
หัวข้อที่มีชื่อว่า “ประโยชน์และข้อควรระวัง”ประโยชน์
- หลีกเลี่ยง gold-plating และ Speculative Generality — code ไม่ถูกถ่วงด้วย abstraction ที่ไม่มีใครใช้จริง
- ทุ่มแรงไปกับปัญหาที่พิสูจน์แล้วว่ามีคุณค่า แทนที่จะเดาอนาคต ซึ่ง Fowler เคยอ้างอิงงานวิจัยว่ามีเพียงราวหนึ่งในสามของ feature ที่วางแผนล่วงหน้าอย่างพิถีพิถันเท่านั้นที่ให้ผลตามที่คาดไว้จริง
- abstraction ที่ได้มักตรงกับปัญหาจริงมากกว่า เพราะออกแบบจากรูปแบบที่เห็นซ้ำ ไม่ใช่จากการคาดเดา
- code ยังคงอ่านง่ายสำหรับความต้องการปัจจุบัน ไม่มี indirection ที่ทำให้ตามงานยาก
ข้อควรระวัง
- pain บางชนิดแก้ยากเกินกว่าจะรอ โดยเฉพาะ pain จากโครงสร้าง dependency หรือ layering ที่วางผิดตั้งแต่ต้น project — ถ้ารู้จากประสบการณ์ว่าจะต้องแยก layer แน่ ๆ การป้องกันไว้ก่อนไม่ถือว่าละเมิด YAGNI
- ถ้าไม่มีวินัยใน refactor เมื่อ pain ปรากฏ code จะสะสมเป็นหนี้ทางเทคนิคจนถึงจุดที่เขียนใหม่ถูกกว่าแก้ไข — PDD ไม่ใช่ข้ออ้างให้ “ไม่แก้”
- ในระดับองค์กร บทความของ Brave New Geek ชี้ว่า pain-driven decision ที่แต่ละทีมเลือกทางลดความเจ็บของตัวเองในระยะสั้น (เช่น เพิ่ม timeout เพิ่มฮาร์ดแวร์) อาจเป็นเพียงการ “ย้าย” ความเจ็บไปให้ทีมอื่นหรือลูกค้าแทนที่จะแก้จริง — เป็น greedy algorithm ที่ดีในระดับทีมแต่ไม่ optimal ในระดับองค์กร ควรมองภาพรวมประกอบด้วย ไม่ใช่แก้เฉพาะจุดที่ตัวเองเจ็บ
- ต้องแยกให้ออกระหว่าง “รอดูก่อน” กับ “เพิกเฉยต่อสัญญาณเตือน” — ถ้าเพิกเฉยนานเกินไป กลิ่น code เล็ก ๆ อาจกลายเป็น Big Ball of Mud
ใช้หลักการเมื่อความเจ็บปรากฏ ไม่ใช่เพราะกลัวว่าจะเจ็บ — เว้นแต่ความเจ็บนั้นจะแก้ทีหลังแทบไม่ได้
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- YAGNI
- Open-Closed Principle
- Keep It Simple
- Speculative Generality
- Refactor Mercilessly
- Big Design Up Front
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/practices/pain-driven-development
- Yagni (bliki) — Martin Fowler
- Pain-Driven Development: Why Greedy Algorithms Are Bad for Engineering Orgs — Brave New Geek
- Pain driven improvements — Facundo’s Blog
- Pain Driven Development — It’s a Delivery Thing