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

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) เฉย ๆ กว่า​จะ​ถึง​ตรรกะ​จริง
  • ขัด​กับ​หลัก YAGNIYAGNI บอกว่า​อย่า​สร้าง​สิ่ง​ที่​ยัง​ไม่​จำเป็น​ต้อง​ใช้​ตอน​นี้ เพราะ​เมื่อ​ความ​ต้องการ​จริง​มา​ถึง มัน​มัก​จะ​มา​ใน​รูป​ที่​ต่าง​จาก​ที่​เรา​คาด​ไว้​ตอน​ออกแบบ​ล่วงหน้า — สุดท้าย​โครงสร้าง​ที่​เผื่อ​ไว้​ก็​ใช้​ไม่​ได้​อยู่ดี ต้อง​รื้อ​ทิ้ง​แล้ว​สร้าง​ใหม่
  • ทำให้​ระบุ 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 เลย​สัก​ครั้ง

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 จริง​สอง​อัน​ที่​เห็น​ตรง​หน้า ไม่ใช่​จาก​การ​เดา

อีก​รูปแบบ​ที่​พบ​บ่อย​คือ 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 ที่​ต้อง​คิด​เผื่อ​ว่า “ถ้า​มี​คน​ส่ง​ค่า​อื่น​มา​จะ​เกิด​อะไร​ขึ้น” ทั้ง​ที่​ไม่​เคย​เกิด​ขึ้น​จริง