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)
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”ตัวอย่างคลาสสิกของ Inconsistent Naming: 2 class ที่ควรมี behavior คู่ขนานกัน (ทั้งคู่เป็น “ตัวละครที่พูดได้”) แต่ผู้เขียนแต่ละคนตั้งชื่อ method ตามใจตัวเอง
// ก่อน refactor — concept เดียวกัน (การพูด) แต่ชื่อ method ต่างกันในแต่ละ classpublic 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 classpublic 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 ลูกของ CharacterCharacter[] 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 ใช้วิธีแก้ปัญหาต่างกันสำหรับสถานการณ์ที่คล้ายกัน