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

Poor Names

ชื่อ​ที่​บดบัง​ว่า​สิ่ง​นั้น​คือ​อะไร​หรือ​ทำ​อะไร

Poor Names เกิด​เมื่อ identifier — ตัวแปร method class parameter field — ถูก​ตั้ง​ชื่อ​ใน​แบบ​ที่​บดบัง​ว่า​มัน​แทน​อะไร​หรือ​ทำ​อะไร ชื่อ​คือ​ช่อง​ทาง​หลัก​ที่ code สื่อ​เจตนา​ต่อ​ผู้​อ่าน (ไม่ใช่ compiler ซึ่ง​ไม่​สนใจ​ว่า​ตัวแปร​ชื่อ x หรือ customerCreditLimit) ชื่อ​ที่​แย่​บังคับ​ให้​ผู้​อ่าน​ทำงาน​หนัก​ขึ้น​เพื่อ​สร้าง​แผนที่​ใน​หัว​ขึ้น​มา​ใหม่​ทุก​ครั้ง​ที่​เปิด file หรือ​แย่​กว่า​นั้น​คือ​พา​ไป​เข้าใจ​ผิด​ไป​เลย

Martin Fowler เรียก​กลิ่น​ตัว​นี้​ใน​หนังสือ Refactoring ฉบับ​พิมพ์​ครั้ง​ที่​สอง​ว่า Mysterious Name และ​วาง​ไว้​เป็นกลิ่น​ตัว​แรก​ที่​กล่าว​ถึง เพราะ​การ​ตั้ง​ชื่อ​คือ​หนึ่ง​ใน​สอง​สิ่ง​ที่​ยาก​ที่สุด​ใน​วงการ (อีก​สิ่ง​คือ cache invalidation และ off-by-one error ตาม​มุก​ที่​เล่า​ต่อ​กัน​มา) Fowler ชี้​ว่า “การ​พยายาม​อ่าน code ปริศนา​สนุก​ดี​ถ้า​เป็น​นิยาย​สืบสวน แต่​ไม่ใช่​ตอน​อ่าน code” — และ​ที่​สำคัญ​กว่า​นั้น เวลา​ที่​ตั้ง​ชื่อ​อะไร​ไม่​ได้​สัก​ที มัก​เป็น​สัญญาณ​ว่า​มี​ปัญหา​การ​ออกแบบ​ที่​ลึก​กว่า​ซ่อน​อยู่ ไม่ใช่​แค่​ปัญหา​เรื่อง​คำ​ศัพท์

รูปแบบ​ของ Poor Names ที่​พบ​บ่อย (อ้างอิง​อนุกรม​วิธาน​จาก Peter Hilton และ​แหล่ง​อื่น):

  • ชื่อ​ไร้​ความหมาย (meaningless names)foo, bar, temp, data, obj, thing ที่​ไม่​ได้​บอก​อะไร​เลย​ว่า​คือ​อะไร
  • ชื่อย่อ​กำกวม (abbreviated names)acc (accumulator? accuracy? account?), pos (position? positive?), auth (authentication? authorization?) — ย่อ​แล้ว​ตีความ​ได้​หลาย​ทาง
  • ชื่อ​สั้น​เกิน​ไป (short/single-letter names)a, i, j, x1, x2 ที่​ไม่มี​บริบท​ช่วย​ขยาย​ความ (ยกเว้น​ตัว​นับ loop สั้น ๆ ที่ scope แคบ​มาก​ซึ่ง​พอ​ยอมรับ​ได้)
  • คำ​ต่อ​ท้าย​เป็น​ตัวเลข (numeric suffixes)employee1, employee2, handler2 ที่​ไม่​บอกว่า​สอง​ตัว​นี้​ต่าง​กัน​อย่างไร
  • คำ​กว้าง​เกิน​ไป (vague words)Manager, Processor, Data, Info, หรือ​กริยา​ทั่วไป​อย่าง get, handle, process ที่​ใช้ได้​กับ​แทบ​ทุก​อย่าง​จน​ไม่​สื่อ​อะไร​เฉพาะ​เจาะจง
  • Hungarian notation ที่​ตก​ยุค — คำนำ​หน้า​บอก​ชนิด เช่น strName, bIsValid, dtCreated ซึ่ง​ซ้ำซ้อน​ใน​ภาษา​ที่​มี static typing และ IDE ที่​บอก​ชนิด​ให้​อยู่​แล้ว
  • ชื่อ​ผิด (wrong names) — ชื่อ​ที่​ฟัง​ดู​สม​เหตุ​สม​ผล​แต่​บอก​ความหมาย​ผิด เช่น method GetTotal() ที่​จริง ๆ แล้ว​คำนวณ + บันทึก​ลง​ฐาน​ข้อมูล​ด้วย (side effect ที่​ชื่อ​ไม่​ได้​บอก)
  • ชื่อ​ไม่​สอดคล้อง​กัน (inconsistency) — เรียก​สิ่ง​เดียวกัน​คนละ​ชื่อ​ใน​หลาย​ที่ (customer, client, user ปน​กัน​สำหรับ concept เดียว) — ดู Inconsistency

วิธี​เช็ค​ง่าย ๆ: ถ้า​คุณ​ต้อง​เปิด​ดู implementation หรือ​ถาม​เพื่อน​ร่วม​ทีม​ว่า “ตัวแปร​นี้/method นี้​มัน​คือ​อะไร​กัน​แน่” นั่น​คือ​สัญญาณ​ของ Poor Names

  • เพิ่ม​ต้นทุน​การ​อ่าน — code ถูก​อ่าน​มากกว่าที่​ถูก​เขียน​หลาย​เท่า ชื่อ​แย่​ทำให้​ทุก​คน​ที่มา​อ่าน​ทีหลัง (รวม​ถึง​ตัว​เรา​เอง​ใน​อีก​หก​เดือน) ต้อง​เสีย​เวลา​สืบสวน​แทนที่​จะ​เข้าใจ​ได้​ทันที
  • ซ่อน bug — ชื่อ​ที่​บอก​ความหมาย​ผิด (เช่น GetTotal() ที่​มี side effect) ทำให้​ผู้​เรียก​ใช้​คาด​เดา​พฤติกรรม​ผิด นำ​ไป​สู่ bug ที่​ตามหา​ยาก เพราะ code “ดูเหมือน” จะ​ทำ​สิ่ง​หนึ่ง​แต่​จริง ๆ ทำ​อีก​สิ่ง​หนึ่ง — เกี่ยวโยง​กับ Obscured Intent
  • บังคับ​ให้​พึ่ง​คอมเมนต์​แทน code — เมื่อ​ชื่อ​ไม่​สื่อ​ความหมาย คน​มัก​แก้​ด้วย​การ​เติม​คอมเมนต์​อธิบาย แต่​คอมเมนต์​ล้าสมัย​ได้​ง่าย​กว่า​ชื่อ​ใน​ซิก​เนเจอร์ของ code ผล​คือ​กลิ่น Comments ซ้อน​ทับ​เข้าไป​อีก​ชั้น — Fowler เปรียบ​คอมเมนต์​ที่​ชดเชย​ชื่อ​แย่​ว่า​เหมือน “ส​เปรย์ระงับ​กลิ่น​กาย” ที่​กลบ​กลิ่น code เน่า​ไว้​ชั่วคราว​แทนที่​จะ​แก้​ที่​ต้นตอ
  • ทำลาย Ubiquitous Language — ใน​บริบท DDD ชื่อ​ใน code ควร​ตรง​กับ​คำ​ที่​ทีม​และ​ผู้เชี่ยวชาญ domain ใช้​พูด​คุย​กัน ชื่อ​แย่​หรือ​ไม่​สอดคล้อง​กัน​ทำให้ code กับ​บทสนทนา​ทาง​ธุรกิจ​แยก​ออก​จาก​กัน จน model ใน code ไม่​สะท้อน​ความ​เข้าใจ domain อีก​ต่อ​ไป — ดู Ubiquitous Language
  • เป็น​สัญญาณ​เตือน​ปัญหา​การ​ออกแบบ — บ่อย​ครั้ง​ที่​ตั้ง​ชื่อ​อะไร​ไม่​ได้​สัก​ที เพราะ class/method นั้น​ทำ​หลาย​หน้าที่​ปน​กัน (ขัด​กับ Single Responsibility Principle) ความ​ยาก​ใน​การ​ตั้ง​ชื่อ​คือ​สัญญาณ​เตือน​ที่​คุ้ม​ค่า​จะ​ฟัง ไม่ใช่​แค่​ปัญหา​คำ​ศัพท์​ผิวเผิน
// ก่อน: ชื่อย่อกำกวม + Hungarian notation ที่ตกยุค
public class OrdProc
{
private double d; // discount? deposit? date?
private bool bIsVal; // valid? validated? value?
public double Calc(double amt, int qty)
{
var t = amt * qty;
if (bIsVal)
{
t = t - (t * d);
}
return t;
}
}

code ข้าง​ต้น compile ผ่าน​และ​อาจ​ทำงาน​ถูกต้อง แต่​ผู้​อ่าน​ต้อง​เดา​ความหมาย​ของ d, bIsVal, t, Calc ทั้งหมด — ใช้ Rename Field, Rename Variable และ Change Function Declaration (การ refactor แบบ​ตรง​ไป​ตรง​มา​ที่ IDE ส่วน​ใหญ่​รองรับ​ด้วย “Rename” อัตโนมัติ) เพื่อ​ทำให้​ชื่อ​สื่อ​เจตนา:

// หลัง: ชื่อสื่อความหมายชัดเจน ไม่ต้องเดา
public class OrderPriceCalculator
{
private double discountRate;
private bool isDiscountEligible;
public double CalculateTotal(double unitPrice, int quantity)
{
var subtotal = unitPrice * quantity;
if (isDiscountEligible)
{
subtotal -= subtotal * discountRate;
}
return subtotal;
}
}

ตัวอย่าง 2 — ชื่อ​ที่​บอก​ความหมาย​ผิด (wrong name ซ่อน side effect)

หัวข้อ​ที่​มีชื่อ​ว่า “ตัวอย่าง 2 — ชื่อ​ที่​บอก​ความหมาย​ผิด (wrong name ซ่อน side effect)”
// ก่อน: ชื่อบอกว่าแค่ "อ่าน" แต่จริง ๆ มี side effect ที่แก้ state และเขียนฐานข้อมูล
public class InvoiceService
{
public decimal GetTotal(Invoice invoice)
{
invoice.Status = InvoiceStatus.Reviewed; // side effect ที่ชื่อไม่ได้บอก
_repository.Save(invoice); // ยิ่งไม่คาดคิด
return invoice.Lines.Sum(l => l.Amount);
}
}

ผู้​เรียก GetTotal() คาด​หวัง​ว่า​เป็นการ​อ่าน​ค่า​ล้วน ๆ (query ไม่มี​ผล​ข้าง​เคียง) แต่​จริง ๆ แล้ว​มัน​เปลี่ยน​สถานะ​และ​บันทึก​ลง​ฐาน​ข้อมูล​ด้วย — นี่​คือ Poor Names ที่​อันตราย เพราะ​ไม่ใช่​แค่​อ่าน​ยาก แต่ โกหก ผู้​อ่าน ทาง​แก้​คือ​แยก​ความ​รับผิดชอบ (Command-Query Separation) แล้ว​ตั้ง​ชื่อ​ให้​ตรง​กับ​สิ่ง​ที่​แต่ละ method ทำ​จริง:

// หลัง: แยก query ออกจาก command และตั้งชื่อให้ตรงกับพฤติกรรมจริง
public class InvoiceService
{
public decimal CalculateTotal(Invoice invoice)
{
return invoice.Lines.Sum(l => l.Amount);
}
public void MarkAsReviewed(Invoice invoice)
{
invoice.Status = InvoiceStatus.Reviewed;
_repository.Save(invoice);
}
}

ตัวอย่าง 3 — คำ​กว้าง​เกิน​ไป​ที่​ทับซ้อน​กับ Ubiquitous Language

หัวข้อ​ที่​มีชื่อ​ว่า “ตัวอย่าง 3 — คำ​กว้าง​เกิน​ไป​ที่​ทับซ้อน​กับ Ubiquitous Language”
// ก่อน: "Manager" และ "Data" ไม่บอกว่า class นี้ทำอะไรจริง ๆ
public class OrderManager
{
public OrderData Process(OrderData data) { /* ... */ }
}

เทียบ​กับ​ชื่อ​ที่​ยืม​มา​จาก​ภาษา​ที่​ผู้เชี่ยวชาญ domain ใช้​พูด​ถึง​กระบวนการ​นี้​จริง ๆ:

// หลัง: ชื่อยืมมาจาก Ubiquitous Language ของทีม สื่อว่า "ทำอะไร" ไม่ใช่แค่ "เกี่ยวกับอะไร"
public class OrderFulfillmentService
{
public FulfilledOrder Fulfill(PendingOrder order) { /* ... */ }
}
flowchart LR
    A[Poor Name พบ] --> B{ตั้งชื่อใหม่ได้ทันทีไหม}
    B -- ได้ --> C[Rename Variable Field หรือ Change Function Declaration]
    B -- ไม่ได้ ตั้งไม่ถูกเลย --> D[สงสัยว่ามีปัญหาการออกแบบลึกกว่า]
    D --> E[แยก responsibility หรือ Extract Function ก่อน]
    E --> C
    C --> F[ชื่อสื่อเจตนา สอดคล้อง Ubiquitous Language]