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

Inconsistency

แก้​ปัญหา​คล้าย​กัน​ด้วย​วิธี​ต่าง​กัน / ตั้ง​ชื่อ​แนวคิด​เดียว​ไม่​เหมือน​กัน

Inconsistency เกิด​เมื่อ ปัญหา​คล้าย​กัน​ถูก​แก้​ด้วย​วิธี​ต่าง​กัน แนวคิด​เดียว​ถูก​ตั้ง​ชื่อ​ไม่​เหมือน​กัน​ใน​แต่ละ​ที่ หรือ convention แปรผัน​โดย​ไม่มี​เหตุผล​รองรับ ความ​สอดคล้อง​คือ​รูปแบบ​หนึ่ง​ของ​การ​สื่อสาร — เมื่อ code สม่ำเสมอ ผู้​อ่าน​นำ​สิ่ง​ที่​เรียนรู้​จาก​ที่​หนึ่ง​ไป​เข้าใจ​อีก​ที่​ได้​ทันที​โดย​ไม่​ต้อง​อ่าน​ซ้ำ

รูปแบบ​ที่​พบ​บ่อย​ที่สุด​ของ​กลิ่น​นี้​คือ Inconsistent Naming — class ที่​ทำ​หน้าที่​คล้าย​กัน​แต่​ตั้ง​ชื่อ method ต่าง​กัน เช่น class หนึ่ง​มี talk() อีก class มี chat() ทั้ง​ที่​ความหมาย​คือ “พูด” เหมือน​กัน หรือ​ความ​ไม่​สม่ำเสมอ​ใน​ระดับ style เช่น​การ​ปน​กัน​ของ camelCase และ PascalCase ใน codebase เดียว การ​ใช้​ตัว​ย่อ​ไม่​คง​เส้น​คง​วา (btn ใน​ที่​หนึ่ง แต่ button ใน​อีก​ที่) หรือ​การ​ใช้​คำ​พ้อง​ความหมาย​แทน​กัน​แบบ​สุ่ม (add, insert, put, store สำหรับ operation เดียวกัน)

กลิ่น​นี้​ยัง​ครอบคลุม​ไป​ถึง​ระดับ การ​ออกแบบ​วิธี​แก้​ปัญหา ไม่ใช่​แค่​ชื่อ — ทีม​แก้​ปัญหา​แบบ​เดียวกัน​ด้วย pattern ต่าง​กัน​ใน​แต่ละ module เช่น module 1 validate input ด้วย exception อีก module validate ด้วย return code ทั้ง​ที่​บริบท​เหมือน​กัน นี่​คือ​ญาติ​สนิท​ของ Oddball Solution ซึ่ง​เจาะจง​กรณี​ที่​มี “วิธี​มาตรฐาน” อยู่​แล้ว​แต่​มี​จุด​หนึ่ง​ที่​แหก​กฎ​โดย​ไม่มี​เหตุผล ขณะ​ที่ Inconsistency กว้าง​กว่า​นั้น​และ​ครอบคลุม​ทั้ง​กรณี​ที่​ไม่มี “มาตรฐาน” ตั้งแต่​แรก​ด้วย​ซ้ำ

  • อ่าน class 2 class ที่​ทำงาน​คล้าย​กัน (เช่น entity สอง​ตัว​ที่​มี behavior คู่​ขนาน) แล้ว​พบ​ว่า method ที่​ทำ​สิ่ง​เดียวกัน​มีชื่อ​ต่าง​กัน
  • ทีม​ต้อง​เปิด​ค้นหา (“อัน​นี้​เรียก​ว่า​อะไร​นะ”) ก่อน​เรียก​ใช้ method ที่​ควร​จะ​เดา​ชื่อ​ได้​จาก​ประสบการณ์​ที่​อื่น​ใน codebase
  • file ต่าง​กัน​ใช้​ตัว​คั่น indentation ต่าง​กัน (tab ปน​กับ space) หรือ brace style ต่าง​กัน​โดย​ไม่มี linter บังคับ
  • Layer เดียวกัน (เช่น repository) ใน​แต่ละ module ใช้ pattern คนละ​แบบ — บาง repository คืน null เมื่อ​ไม่​พบ บาง​อัน​คืน Optional<T> บาง​อัน​ขว้าง exception
  • Code review ต้อง​เถียง​เรื่อง “จะ​เรียก​อัน​นี้​ว่า​อะไร​ดี” ซ้ำ ๆ ใน​แต่ละ PR เพราะ​ไม่มี​ข้อ​ตกลง​ร่วม
  • Diff ระหว่าง2 PR ที่​แก้ feature คล้าย​กัน​แสดง​วิธี​แก้​ที่​ต่าง​กัน​โดย​สิ้นเชิง ทั้ง​ที่ requirement เหมือน​กัน

ความ​สอดคล้อง (consistency) เป็น​กลไก mental shortcut ที่​สำคัญ​มาก​สำหรับ​ผู้​อ่าน code เมื่อ​สมอง​เรียนรู้ pattern จาก​ที่​หนึ่ง​แล้ว​นำ​ไป​ทำนาย​ที่​อื่น​ได้​ถูกต้อง ความเร็ว​ใน​การ​อ่าน​และ​ความ​มั่นใจ​ใน​การ​แก้ไข​จะ​สูง​ขึ้น​มาก ใน​ทาง​กลับ​กัน Inconsistency ทำลาย shortcut นั้น:

  • เสีย​เวลา​ค้นหา — นัก​พัฒนา​ต้อง​เปิด​หลาย file เพื่อ​เช็ค​ว่า method/แนวคิด​นี้​ใน​บริบท​นี้​เรียก​ว่า​อะไร แทนที่​จะ​เดา​ได้​ถูก​จาก​ประสบการณ์​ที่​อื่น
  • เพิ่ม​ความ​เสี่ยง bug — เมื่อ​สอง​ที่​ที่ “ควร​จะ​เหมือน​กัน” กลับ​ต่าง​กัน คน​แก้ code อาจ​แก้​ที่​เดียว​แล้ว​ลืม​อีก​ที่ หรือ​เข้าใจ​ผิด​ว่า​พฤติกรรม​เหมือน​กัน​ทั้ง​ที่​ไม่ใช่
  • ทำให้ onboarding ช้า​ลง — สมาชิก​ใหม่​ใน​ทีม​ต้อง​เรียนรู้​ข้อ​ยกเว้น​จำนวน​มาก​แทนที่​จะ​เรียนรู้​กฎ​เดียว​แล้ว​นำ​ไป​ใช้​ซ้ำ​ได้
  • บั่นทอน tooling — refactor อัตโนมัติ (เช่น rename-across-project), IDE auto-complete, และ static analysis ทำงาน​ได้​แม่นยำ​น้อย​ลง​เมื่อ​ชื่อ/รูปแบบ​ไม่​คง​เส้น​คง​วา
  • สะท้อน​ปัญหา​การ​สื่อสาร​ใน​ทีม — มัก​เป็น​สัญญาณ​ว่า​ไม่มี​การ​รีวิว code ข้าม​ทีม​อย่าง​จริงจัง หรือ​ไม่มี​ใคร​รู้สึก​เป็น​เจ้าของ code ทั้งหมด​ร่วม​กัน (ตรง​ข้าม​กับ Collective Code Ownership)

ตัวอย่าง​คลาสสิก​ของ Inconsistent Naming: 2 class ที่​ควร​มี behavior คู่​ขนาน​กัน (ทั้ง​คู่​เป็น “ตัว​ละคร​ที่​พูด​ได้”) แต่​ผู้​เขียน​แต่ละ​คน​ตั้ง​ชื่อ method ตามใจ​ตัวเอง

// ก่อน refactor — concept เดียวกัน (การพูด) แต่ชื่อ method ต่างกันในแต่ละ class
public class Human
{
public void Talk() => Console.WriteLine("Human พูด");
}
public class Elf
{
public void Chat() => Console.WriteLine("Elf พูด");
}
public class Orc
{
public void Speak() => Console.WriteLine("Orc พูด");
}
// ผู้เรียกใช้ต้องจำชื่อ method ที่ต่างกันสามแบบสำหรับพฤติกรรมเดียวกัน
var human = new Human();
var elf = new Elf();
var orc = new Orc();
human.Talk();
elf.Chat();
orc.Speak();

refactor ด้วย Rename Method ให้​ชื่อ​สอดคล้อง​กัน​ก่อน แล้ว​ใช้ Extract Superclass (หรือ interface ร่วม) เพื่อ​บังคับ​ความ​สอดคล้อง​นั้น​ไว้​ใน​ระดับ type ไม่ใช่​แค่​ข้อ​ตกลง​ปากเปล่า:

// หลัง refactor — สัญญาเดียวกัน บังคับด้วย abstract class
public abstract class Character
{
// ทุก class ลูกต้อง Talk ด้วยชื่อเดียวกัน — ผู้เรียกใช้เดาชื่อ method ถูกเสมอ
public abstract void Talk();
}
public class Human : Character
{
public override void Talk() => Console.WriteLine("Human พูด");
}
public class Elf : Character
{
public override void Talk() => Console.WriteLine("Elf พูด");
}
public class Orc : Character
{
public override void Talk() => Console.WriteLine("Orc พูด");
}
// ผู้เรียกใช้จำชื่อเดียว ใช้ได้กับทุก class ลูกของ Character
Character[] party = { new Human(), new Elf(), new Orc() };
foreach (var character in party)
character.Talk();

ตัวอย่าง​ที่​สอง — ความ​ไม่​สอดคล้อง​ใน​ระดับ วิธี​แก้​ปัญหา ไม่ใช่​แค่​ชื่อ: 2 repository ทำ​หน้าที่​คล้าย​กัน​แต่​รายงาน “ไม่​พบ​ข้อมูล” ต่าง​วิธี​กัน

// ก่อน refactor — 2 repository จัดการ "ไม่พบ" คนละแบบ ทั้งที่ควรสอดคล้องกัน
public class CustomerRepository
{
public Customer? FindById(int id)
{
// คืน null เมื่อไม่พบ
return _customers.FirstOrDefault(c => c.Id == id);
}
}
public class OrderRepository
{
public Order GetById(int id)
{
var order = _orders.FirstOrDefault(o => o.Id == id);
if (order is null)
throw new KeyNotFoundException($"ไม่พบ Order id={id}"); // ขว้าง exception แทน
return order;
}
}

refactor ด้วย​การ​ตกลง convention เดียว​สำหรับ layer นี้​ทั้งหมด (ใน​ที่​นี้​เลือก return แบบ nullable/optional เพื่อ​ให้​ผู้​เรียก​ใช้​ตัดสิน​ใจ​เอง​ว่า​จะ​จัดการ “ไม่​พบ” อย่างไร) แล้ว​ยึด​ชื่อ method ให้​ตรง​กัน:

// หลัง refactor — ทุก repository ใช้ pattern เดียวกัน: FindById คืนค่า nullable เสมอ
public interface IRepository<T> where T : class
{
T? FindById(int id);
}
public class CustomerRepository : IRepository<Customer>
{
public Customer? FindById(int id) =>
_customers.FirstOrDefault(c => c.Id == id);
}
public class OrderRepository : IRepository<Order>
{
public Order? FindById(int id) =>
_orders.FirstOrDefault(o => o.Id == id);
}
// ผู้เรียกใช้จัดการ "ไม่พบ" ด้วยรูปแบบเดียวกันทุกที่
var order = orderRepository.FindById(42);
if (order is null)
{
// ตัดสินใจเองตามบริบท: throw, คืนค่า default, หรือส่ง response 404
}

แผนภาพ​สรุป​ว่า​เมื่อ​ไม่มี convention กลาง แต่ละ class ก็​จะ​แยก​วิ่ง​ไป​คนละ​ทาง แต่​เมื่อ​ดึง​สัญญา​ร่วม​ออก​มา ทุก class จะ​พึ่งพา​สัญญา​เดียวกัน:

flowchart LR
    Human --> Talk
    Elf --> Chat
    Orc --> Speak
    Character --> TalkShared
    HumanB --> TalkShared
    ElfB --> TalkShared
    OrcB --> TalkShared
  • Oddball Solution — กรณี​เฉพาะ​ที่​มี convention มาตรฐาน​อยู่​แล้ว​แต่​มี​จุด​หนึ่ง​แหก​กฎ
  • Collective Code Ownership — แนว​ปฏิบัติ​ที่​ทำให้​ทีม​ยึดถือ convention ร่วม​กัน​แทน​ต่าง​คน​ต่าง​เขียน
  • Naming Things — หลักการ​ตั้ง​ชื่อ​ที่​ช่วย​ป้องกัน​ความ​ไม่​สอดคล้อง​ตั้งแต่​ต้น
  • Duplicate Code — เมื่อ​วิธี​แก้​ปัญหา​เดียวกัน​ถูก​ทำซ้ำ​แทนที่​จะ share ก็​มัก​มา​พร้อม​ความ​ไม่​สอดคล้อง​เมื่อ​สำเนา​หนึ่ง​ถูก​แก้​แต่​อีก​สำเนา​ไม่​ถูก​แก้​ตาม
  • Shotgun Surgery — ผลกระทบ​หนึ่ง​ของ​ความ​ไม่​สอดคล้อง: การ​เปลี่ยนแปลง​เล็กน้อย​ต้อง​กระจาย​แก้​หลาย​จุด​เพราะ​ไม่มี convention รวม​ศูนย์
  • Switch Statements — มัก​ปรากฏ​คู่​กับ Inconsistency เมื่อ​แต่ละ case ใน switch ใช้​วิธี​แก้​ปัญหา​ต่าง​กัน​สำหรับ​สถานการณ์​ที่​คล้าย​กัน