Speculative Generality
สร้าง abstraction เผื่ออนาคตที่อาจไม่มีวันมาถึง
กลิ่นนี้คืออะไร
หัวข้อที่มีชื่อว่า “กลิ่นนี้คืออะไร”Speculative Generality คือการสร้าง class, method, field, parameter หรือ hierarchy ทั้งชั้น “เผื่อไว้ก่อน” สำหรับความต้องการในอนาคตที่ยังไม่เกิดขึ้นจริง และหลายครั้งก็ไม่มีวันเกิดขึ้นเลย รูปแบบที่พบบ่อยได้แก่
- abstract class หรือ interface ที่มี implementation เดียวมาโดยตลอด — สร้างไว้เผื่อว่าจะมี implementation ที่สองในอนาคต
- parameter หรือ hook (เช่น callback, delegate) ที่ไม่มี caller ที่ไหนส่งค่าอย่างอื่นนอกจาก default เลยสักครั้ง
- method ที่ทำอะไรมากกว่าที่ผู้เรียกต้องการ เพราะ “เผื่อวันหนึ่งจะมีคนอยากได้แบบนี้”
- field ที่เก็บค่าไว้ให้ subclass ในอนาคตใช้ ทั้งที่ยังไม่มี subclass ไหนใช้จริง
Martin Fowler อธิบายไว้ใน catalog Refactoring ว่านี่คือความพยายามสร้างโครงสร้างรองรับ feature ที่จินตนาการไว้ (“just in case”) แต่ feature นั้นไม่เคยมาถึง สิ่งที่ได้จึงเป็น code ที่อ่านยากขึ้นและดูแลยากขึ้นโดยไม่ได้อะไรตอบแทน ต่างจาก generalization ที่ดี — ซึ่งเกิดจาก duplication หรือความต้องการที่ มีอยู่จริงในวันนี้ — speculative generality สร้างขึ้นจากการคาดเดาอนาคต และอนาคตแบบนั้นมักผิดจากที่คาดไว้เสมอ
วิธีสังเกต
หัวข้อที่มีชื่อว่า “วิธีสังเกต”สัญญาณที่ชัดที่สุดคือ “unused class, method, field หรือ parameter” ลองมองหา
- interface ที่มี implementer เดียวมาหลายเดือนหรือหลายปี และไม่มีแผนจะเพิ่มตัวที่สอง
- abstract method ที่ subclass ทุกตัว override เหมือนกันเป๊ะ หรือมี subclass เดียวที่ override เลย
- parameter ที่ทุก call site ส่ง
nullหรือค่า default เดียวกันเข้าไปตลอด - ชื่อ method หรือ class ที่กว้างเกินจำเป็น เช่น
Process,Handle,DoWorkโดยตั้งใจให้ “รองรับได้ทุกกรณี” ทั้งที่ตอนนี้มีกรณีเดียว - code ที่ผู้ใช้งานจริงมีแค่ test case — ถ้าตัด test นั้นออกแล้วไม่มีใครเรียกใช้อีกเลย นั่นคือสัญญาณเต็มตัว
- ความคิดเห็นใน code ทำนอง “TODO: รองรับ provider อื่นในอนาคต” ที่ไม่มีวันที่ ไม่มี ticket อ้างอิง และไม่มีใครจำได้ว่าเขียนไว้ทำไม
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”โครงสร้างที่สร้างเผื่ออนาคตมีต้นทุนตั้งแต่วันแรก แม้จะยังไม่ได้ใช้งานจริง
- เพิ่มภาระทางความคิด — ผู้อ่าน code ต้องไล่ผ่านชั้น abstraction, interface, hook ที่ไม่ได้ทำอะไรมากกว่าการส่งต่อ (delegate) เฉย ๆ กว่าจะถึงตรรกะจริง
- ขัดกับหลัก YAGNI — YAGNI บอกว่าอย่าสร้างสิ่งที่ยังไม่จำเป็นต้องใช้ตอนนี้ เพราะเมื่อความต้องการจริงมาถึง มันมักจะมาในรูปที่ต่างจากที่เราคาดไว้ตอนออกแบบล่วงหน้า — สุดท้ายโครงสร้างที่เผื่อไว้ก็ใช้ไม่ได้อยู่ดี ต้องรื้อทิ้งแล้วสร้างใหม่
- ทำให้ระบุ scope ของการเปลี่ยนแปลงยากขึ้น — เมื่อมี hook หรือ extension point ที่ไม่มีใครใช้ปนอยู่กับของที่ใช้จริง ผู้พัฒนาใหม่จะแยกไม่ออกว่าอันไหนสำคัญ อันไหนเป็นเพียงการเผื่อไว้
- ทดสอบยากขึ้น — ทุก branch, ทุก parameter, ทุก override ต้องมี test coverage ในทางทฤษฎี แต่ code ที่ไม่มีใครใช้มักไม่มีใครเขียน test ให้ครบ กลายเป็นจุดเสี่ยงเงียบ ๆ
- ปะปนกับ generalization ที่ดี — การสร้าง abstraction เพื่อลด duplication ที่มีอยู่จริง (ตามหลัก OCP) นั้นมีคุณค่า แต่ speculative generality ขโมยความน่าเชื่อถือของ abstraction ที่ดีไป เพราะทีมเริ่มไม่ไว้ใจว่า interface ใน codebase นี้มีไว้ “เพราะจำเป็นจริง” หรือ “เผื่อไว้เฉย ๆ”
หลักที่มักอ้างคู่กันคือ Rule of Three — สร้าง abstraction เมื่อเจอ pattern ซ้ำเป็นครั้งที่สามแล้วเท่านั้น ไม่ใช่ตั้งแต่ยังไม่เจอ duplication เลยสักครั้ง
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”กรณีที่ 1 — hierarchy ที่มี implementation เดียว
หัวข้อที่มีชื่อว่า “กรณีที่ 1 — hierarchy ที่มี implementation เดียว”code ด้านล่างสร้าง PaymentProcessorBase ไว้ “เผื่อว่าวันหนึ่งจะมีวิธีชำระเงินแบบอื่น” แต่ผ่านมาสองปี ระบบก็ยังมีแค่บัตรเครดิตทางเดียว
// เผื่อไว้ก่อน แต่ไม่เคยมี implementation ตัวที่สองเกิดขึ้นจริงpublic abstract class PaymentProcessorBase{ public abstract void Process(Order order);
protected virtual void LogTransaction(Order order) { Console.WriteLine($"Processing order {order.Id}"); }}
public class CreditCardPaymentProcessor : PaymentProcessorBase{ public override void Process(Order order) { LogTransaction(order); // ตรรกะตัดบัตรเครดิตจริง ๆ Console.WriteLine($"Charging card for order {order.Id}"); }}
public class CheckoutService{ private readonly PaymentProcessorBase _processor = new CreditCardPaymentProcessor();
public void Checkout(Order order) => _processor.Process(order);}ผังก่อน refactor — มีชั้น abstraction ที่ไม่มีใครใช้ประโยชน์คั่นอยู่ระหว่าง service กับ logic จริง
flowchart TD CheckoutService --> PaymentProcessorBase PaymentProcessorBase --> CreditCardPaymentProcessor
ใช้ Collapse Hierarchy รวมกับ Inline Class เพื่อยุบชั้นที่ไม่จำเป็นทิ้ง เหลือแค่สิ่งที่ใช้งานจริง
public class CheckoutService{ public void Checkout(Order order) { LogTransaction(order); // ตรรกะตัดบัตรเครดิตจริง ๆ Console.WriteLine($"Charging card for order {order.Id}"); }
private void LogTransaction(Order order) => Console.WriteLine($"Processing order {order.Id}");}หมายเหตุ: ถ้าวันหนึ่งมีความต้องการวิธีชำระเงินที่สองเกิดขึ้นจริง การแยก interface กลับออกมา (Extract Interface) ในตอนนั้นทำได้ง่ายกว่าและถูกต้องกว่า เพราะออกแบบจาก use case จริงสองอันที่เห็นตรงหน้า ไม่ใช่จากการเดา
กรณีที่ 2 — parameter และ generic ที่ไม่มีใครใช้
หัวข้อที่มีชื่อว่า “กรณีที่ 2 — parameter และ generic ที่ไม่มีใครใช้”อีกรูปแบบที่พบบ่อยคือ hook หรือ generic type parameter ที่ใส่ไว้เผื่อความยืดหยุ่น แต่ทุก call site ก็ส่งค่าเดียวกันตลอด
// generic type T ไม่เคยถูกใช้เป็นอย่างอื่นนอกจาก string// onBeforeSend ไม่เคยมี caller ไหนส่งค่ามาเลยสักครั้งpublic interface INotifier<T>{ void Notify(T payload, Action<T>? onBeforeSend = null);}
public class EmailNotifier : INotifier<string>{ public void Notify(string payload, Action<string>? onBeforeSend = null) { onBeforeSend?.Invoke(payload); Console.WriteLine($"Sending email: {payload}"); }}ใช้ Remove Parameter ตัด hook ที่ไม่มีใครใช้ทิ้ง และใช้ Collapse Hierarchy ยุบ interface ที่มี implementer เดียวออกไปด้วย เพราะ generic parameter ก็ไม่เคยแปรผันเลย
public class EmailNotifier{ public void Notify(string payload) => Console.WriteLine($"Sending email: {payload}");}code สั้นลง อ่านตรงไปตรงมาขึ้น และไม่มี branch หรือ type parameter ที่ต้องคิดเผื่อว่า “ถ้ามีคนส่งค่าอื่นมาจะเกิดอะไรขึ้น” ทั้งที่ไม่เคยเกิดขึ้นจริง
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- YAGNI
- Pain Driven Development
- Keep It Simple
- Open/Closed Principle
- Lazy Class
- Combining Different Levels of Abstraction