Oddball Solution
ปัญหาที่มีทางออกดีทางเดียว กลับถูกแก้หลายวิธีปนกัน
กลิ่นนี้คืออะไร
หัวข้อที่มีชื่อว่า “กลิ่นนี้คืออะไร”Oddball Solution เกิดเมื่อปัญหาเดียวกันถูกแก้ด้วยหลายวิธีที่ต่างกันในจุดต่าง ๆ ของ codebase เดียวกัน — บ้างใช้ helper method บ้าง inline logic เอง บ้างเรียก library คนละตัว บ้าง copy-paste แล้วปรับนิดหน่อย แต่ละจุดอาจทำงานถูกต้องเมื่อดูแยก ๆ แต่เมื่อมองรวมแล้วคือความไม่สอดคล้อง (inconsistency) ที่ทำให้ใครก็ตามที่เข้ามาแก้ code ต้องเดาว่า “ครั้งนี้ทีมใช้วิธีไหน”
คำนี้มาจากหนังสือ Refactoring to Patterns ของ Joshua Kerievsky (2004) ซึ่งนิยามไว้ตรง ๆ ว่า: เมื่อปัญหาหนึ่งถูกแก้ด้วยวิธีหนึ่งทั่วทั้งระบบ แล้วปัญหาเดียวกันนั้นถูกแก้ด้วยอีกวิธีหนึ่งในบางจุดของระบบเดียวกัน วิธีที่แปลกแยกออกไปนั้นคือ oddball หรือ “solution ที่ไม่สอดคล้อง”
จุดที่ทำให้ Oddball Solution ต่างจาก Duplicate Code ตรง ๆ คือ มันไม่ใช่การก็อปวางบรรทัดต่อบรรทัด แต่เป็นการแก้ปัญหาเชิงตรรกะเดียวกันด้วยแนวทางคนละแบบ — ซ่อนตัวแนบเนียนกว่า duplicate code ทั่วไป จึงตรวจจับยากกว่า
ในหนังสือเล่มเดียวกัน Kerievsky ยังตั้งชื่อกลิ่นพี่น้องอีกตัวว่า Solution Sprawl ซึ่งเกิดเมื่อการแก้ปัญหาหนึ่ง ๆ ถูกกระจายไปหลาย class หลาย module จนต้องกระโดดไปมาหลายที่กว่าจะเข้าใจภาพรวม — Oddball Solution มักเป็นจุดเริ่มต้นที่นำไปสู่ Solution Sprawl เพราะเมื่อไม่มีจุดศูนย์กลางเดียว แต่ละทีมก็แตกตัวออกไปแก้ปัญหาซ้ำในที่ของตัวเอง
flowchart LR
Problem[ปัญหาเดียวกัน การตรวจอีเมล] --> PathA[RegistrationService ทำแบบหนึ่ง]
Problem --> PathB[NewsletterSubscriber ทำอีกแบบ]
Problem --> PathC[SupportTicketForm ทำอีกแบบ]
PathA -.-> Unify[รวมเป็น EmailValidator ตัวเดียว]
PathB -.-> Unify
PathC -.-> Unify
วิธีสังเกต
หัวข้อที่มีชื่อว่า “วิธีสังเกต”- Method หรือ function ที่ทำหน้าที่เดียวกันแต่ตั้งชื่อไม่เหมือนกัน เช่น class หนึ่งมี
Ask()อีก class มีRead()ทั้งที่ทำงานแบบเดียวกัน - class ที่ควรมี constructor หรือ interface คล้ายกัน (เพราะทำงานประเภทเดียวกัน) แต่ signature ต่างกันโดยไม่มีเหตุผลด้าน domain
- algorithm เดียวกัน (เช่น การ parse วันที่, การคำนวณราคาส่วนลด, การ validate อีเมล) ถูกเขียนซ้ำคนละแบบในหลาย file
- เวลาจะเรียกใช้ชุด class ที่ทำงานคล้ายกัน (เช่น repository หลายตัว, validator หลายตัว) กลับต้องเรียกด้วยรูปแบบไม่เหมือนกันทุกตัว
- Code review เจอคอมเมนต์ซ้ำ ๆ ว่า “ทำไมตรงนี้ไม่ใช้ function ที่มีอยู่แล้ว” หรือ “อ้าว ที่จริงมี helper ตัวนี้อยู่แล้วเหรอ”
- เกิดบ่อยเมื่อทีมโตขึ้นเร็วโดยไม่มีมาตรฐานร่วม, developer หลายคนแก้ปัญหาคล้ายกันแบบแยกกันโดยไม่รู้ว่ามีคนแก้ไปแล้ว, หรือ code ค่อย ๆ วิวัฒนาการมานานโดยไม่เคยถูก refactor ให้เป็นหนึ่งเดียว
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”ภาระทางปัญญาเพิ่มขึ้น — หลักการพื้นฐานคือควรมี “ทางเดียว” ในการจัดการปัญหาหนึ่งตลอดทั้ง project เมื่อมีหลายทาง นักพัฒนาต้องจำได้ว่าแต่ละจุดใช้วิธีไหน และเดาไม่ได้ว่า code ใหม่ที่ตัวเองเขียนควรเลียนแบบจุดไหน
Duplication ที่มองไม่ออกง่าย ๆ — แม้ไม่ใช่การก็อปบรรทัดต่อบรรทัด แต่ตรรกะเดียวกันถูกดูแลอยู่หลายที่ เท่ากับต้นทุนการดูแลรักษาซ้ำซ้อนโดยไม่จำเป็น
ความเสี่ยงตอนแก้ไข — เมื่อกติกาของปัญหานั้นเปลี่ยน (เช่น สูตรคำนวณภาษีเปลี่ยน) ต้องไล่แก้ทุกจุดที่มี “วิธีของตัวเอง” แยกกัน มีโอกาสสูงที่จะแก้ไม่ครบ เกิด bug ที่จุดหนึ่งแต่จุดอื่นถูกต้อง — อาการนี้มักลากไปสู่ Shotgun Surgery ที่การเปลี่ยนแปลงเล็ก ๆ กระจายแรงกระเพื่อมไปหลาย file
เมล็ดพันธุ์ของ abstraction ที่น่าสงสัย — เมื่อพยายามรวมวิธีที่ต่างกันเข้าด้วยกันแบบเร่งรีบ อาจได้ abstraction ที่บิดเบี้ยวเพื่อ “ครอบ” ทุกวิธีเข้าด้วยกันแทนที่จะเลือกวิธีที่ดีที่สุดจริง ๆ
ทดสอบยากขึ้นเป็นทวีคูณ — ถ้าปัญหาหนึ่งมีสามวิธีแก้ ทีมต้องเขียนและดูแล test suite แยกกันสามชุดสำหรับกรณีขอบเขต (edge case) เดียวกัน แทนที่จะเขียนทดสอบเข้มข้นแค่ที่จุดเดียวแล้วมั่นใจได้ทั้งระบบ
Oddball Solution จึงเป็นญาติใกล้ชิดกับ Inconsistency — Inconsistency มองภาพกว้างกว่า (สไตล์, การตั้งชื่อ, โครงสร้าง) ส่วน Oddball Solution เจาะจงไปที่ “วิธีแก้ปัญหาเชิงตรรกะเดียวกัน”
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”สมมติระบบมีการ validate อีเมลอยู่สามจุด แต่ละจุดเขียนคนละแบบ:
// จุดที่ 1 — ใน RegistrationServicepublic bool IsValidEmail(string email){ return email.Contains("@") && email.Contains(".");}
// จุดที่ 2 — ใน NewsletterSubscriberpublic bool CheckEmail(string address){ var pattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$"; return Regex.IsMatch(address, pattern);}
// จุดที่ 3 — ใน SupportTicketForm (inline ตรง ๆ ไม่มีแม้แต่ method)if (!ticket.ContactEmail.Contains("@")){ throw new ValidationException("อีเมลไม่ถูกต้อง");}ทั้งสามจุดแก้ปัญหาเดียวกัน (“อีเมลนี้ถูกรูปแบบไหม”) ด้วยความเข้มงวดต่างกัน ชื่อ method ต่างกัน และบางจุดไม่มี method เลยด้วยซ้ำ — ถ้าวันหนึ่งกฎการ validate ต้องเปลี่ยน (เช่น ต้องรองรับ email ที่มี + หรือ domain ภาษาไทย) ทีมมีโอกาสสูงที่จะแก้ไม่ครบทั้งสามจุด
ขั้นตอนแก้ตามแนวทางของ Kerievsky คือ (1) เลือกวิธีที่ดีที่สุดหนึ่งเดียว — ไม่จำเป็นต้องเป็นวิธีที่ถูกใช้บ่อยที่สุด บางครั้งวิธีที่ใช้น้อยที่สุดกลับเป็นวิธีที่คุณภาพดีกว่า (2) ใช้ Substitute Algorithm แทนที่ทุกจุดด้วยวิธีเดียวกัน และ (3) ย้ายตรรกะไปรวมไว้ที่เดียว (Extract Method/Class) เพื่อไม่ให้เกิดการดูแลซ้ำซ้อนอีก:
// รวมเป็นจุดเดียว — ใช้ EmailValidator ตัวเดียวทั้งระบบpublic static class EmailValidator{ private static readonly Regex Pattern = new(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.Compiled);
public static bool IsValid(string email) => Pattern.IsMatch(email);}
// ทุกจุดเรียกใช้แบบเดียวกันpublic bool IsValidEmail(string email) => EmailValidator.IsValid(email);
public bool CheckEmail(string address) => EmailValidator.IsValid(address);
if (!EmailValidator.IsValid(ticket.ContactEmail)){ throw new ValidationException("อีเมลไม่ถูกต้อง");}กรณีที่ซับซ้อนกว่านั้นคือเมื่อวิธีที่แตกต่างกันมาจากclass ที่มี interface ไม่ตรงกัน เช่น repository สามตัวที่ทำงานคล้ายกันแต่มีชื่อ method คนละชุด (Fetch(), Load(), GetById()) — กรณีนี้ Kerievsky แนะนำให้ Unify Interfaces with Adapter คือสร้าง interface ร่วมแล้วห่อแต่ละ class ด้วย Adapter เพื่อให้ code ฝั่งที่เรียกใช้เห็นเป็นหน้าตาเดียวกัน จากนั้นค่อยมองหาโอกาสรวมตรรกะที่ซ้ำซ้อนกันจริง ๆ ต่อไป สถานการณ์แบบนี้มักถูกเรียกว่า Alternative Class with Different Interfaces ซึ่งเป็นกลิ่นพี่น้องกันโดยตรง
ข้อควรระวังตอน refactor คือ ก่อนลบวิธีเก่าออก ควรเขียน test ครอบวิธีเก่าทุกจุดก่อน (characterization test) เพื่อยืนยันว่าพฤติกรรมเดิมของแต่ละจุดคืออะไร แล้วค่อยแทนที่ทีละจุดพร้อมรัน test ยืนยันทุกครั้ง — การรวมวิธีแก้เข้าด้วยกันแบบรวดเดียวทั้งระบบมีความเสี่ยงสูง เพราะบางจุดอาจแอบพึ่งพา edge case ของวิธีเดิมอยู่โดยไม่มีใครรู้ตัว
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Inconsistency
- Duplicate Code
- Alternative Class with Different Interfaces
- Shotgun Surgery
- Adapter
- Don’t Repeat Yourself
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/code-smells/oddball-solution
- Code Smells | Oddball Solution — luzkan.github.io
- Oddball Solution | Refactoring to Patterns — flylib.com
- Joshua Kerievsky, Refactoring to Patterns (Addison-Wesley, 2004)