Alternative Class with Different Interfaces
2 class ทำหน้าที่เดียวกัน แต่ interface ต่างจนสลับกันไม่ได้
กลิ่นนี้คืออะไร
หัวข้อที่มีชื่อว่า “กลิ่นนี้คืออะไร”กลิ่นนี้เกิดเมื่อ 2 class ขึ้นไปทำบทบาทเหมือนหรือคล้ายกันมาก แต่เปิด function ผ่านชื่อ method ลำดับ parameter หรือ signature ที่ต่างกัน ทั้งที่ในทางแนวคิดแล้วมันควรเป็น “ทางเลือก” ของกันและกัน — สลับใช้อันไหนก็ได้แล้วแต่สถานการณ์ แต่เพราะ interface ไม่ตรงกัน code ฝั่งที่เรียกใช้จึงสลับแทนกันไม่ได้จริง ต้องเขียนเงื่อนไขแยกเป็นราย class แทน
Martin Fowler จัดกลิ่นนี้อยู่ในหมวด Object-Orientation Abusers ในหนังสือ Refactoring และวิกิพีเดียเรียกกลิ่นนี้ว่า Duplicate Abstraction ด้วยเช่นกัน คือเมื่อ2 abstraction มีชื่อซ้ำ หรือมีการทำงานซ้ำ หรือทั้งสองอย่าง สาเหตุที่พบบ่อยที่สุดคือ นักพัฒนาคนหนึ่งเขียน class ขึ้นมาโดยไม่รู้ว่ามี class ที่ทำหน้าที่เดียวกันอยู่แล้วใน codebase — อาจเพราะทีมใหญ่ขึ้น สื่อสารกันไม่ทั่วถึง หรือแยกกันทำงานคนละ module โดยไม่รู้จักงานของกัน
วิธีสังเกต
หัวข้อที่มีชื่อว่า “วิธีสังเกต”- มี2 class (หรือมากกว่า) ที่ทำงานคล้ายกันมาก แต่ชื่อ method คนละชื่อ เช่น
SendEmailกับTransmit - code ผู้เรียกมี
if/elseหรือswitchที่ตรวจชนิดของ object ก่อน แล้วเรียก method คนละชื่อในแต่ละ branch - parameter ทำหน้าที่เดียวกันแต่ลำดับหรือชนิดต่างกัน เช่น class หนึ่งรับ
(string to, string subject, string body)อีก class รับ(string message, string recipient) - เวลาต้องเพิ่มช่องทางใหม่ (channel/format/provider ใหม่) ต้องเพิ่มเงื่อนไขใหม่ในทุกจุดที่เรียกใช้ แทนที่จะเพิ่ม implementation ใหม่เพียงจุดเดียว
- ทีมสองคน (หรือ2 sprint) ผลิต class ที่ทำงานซ้ำกันขึ้นมาโดยไม่รู้ตัว มักเจอตอนรีวิว code หรือตอน merge
ทำไมถึงเป็นปัญหา
หัวข้อที่มีชื่อว่า “ทำไมถึงเป็นปัญหา”ปัญหาหลักคือ coupling ฝั่งผู้เรียกเพิ่มขึ้นโดยไม่จำเป็น — code ที่ควรจะปฏิบัติต่อทั้ง2 class อย่างเดียวกัน (polymorphism) กลับต้องรู้จักรายละเอียดของแต่ละ class แยกกัน ทุกจุดที่ใช้งานต้องคอย branch ตามชนิด ซึ่งขัดกับ DRY เพราะตรรกะ “จะเรียกอันไหนด้วยชื่อไหน” ถูกก็อปปีกระจายอยู่หลายที่
มันยังขัดกับแนวคิดของ Liskov Substitution — ถ้า2 class ควรจะแทนกันได้ในเชิงพฤติกรรม แต่ interface ต่างกัน ก็ไม่มีทาง substitute จริง ๆ ได้จนกว่าจะเขียน adapter หรือ branch เอาไว้ก่อน และเมื่อมี class ที่ 3 มาเพิ่ม (เช่น ช่องทางแจ้งเตือนใหม่) จำนวนจุดที่ต้องแก้ก็ยิ่งทวีคูณ ซึ่งเป็นอาการเดียวกับ Shotgun Surgery — เปลี่ยนสิ่งหนึ่งแต่ต้องกระเพื่อมไปแก้หลาย file
สุดท้ายมันคือรูปแบบหนึ่งของ Duplicate Code ในระดับ abstraction ไม่ใช่ระดับ statement — code สอง (หรือหลาย) ก้อนทำงานเหมือนกันเป๊ะ เพียงแต่ห่อด้วยชื่อคนละชื่อ ทำให้คนอ่าน code ใหม่ต้องเสียเวลาไล่ดูว่าทำไมถึงมี2 class สำหรับงานเดียวกัน และเสี่ยงแก้ bug แค่ class เดียวโดยลืมอีก class
ผลกระทบยังลามไปถึงชุดทดสอบด้วย — เมื่อไม่มี interface กลาง unit test ของ code ผู้เรียกต้อง mock หรือ stub class ทั้งสองแยกกัน แทนที่จะ mock ผ่านสัญญาเดียว และเมื่อ code โตขึ้นเรื่อย ๆ จำนวน class “ทางเลือก” ที่ไม่ได้อยู่ในสัญญาเดียวกันก็จะยิ่งเพิ่ม ทำให้การเพิ่ม feature ใหม่แต่ละครั้งมีต้นทุนสูงขึ้นเรื่อย ๆ ทั้งที่ไม่จำเป็น
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”สมมติมี2 class สำหรับแจ้งเตือนลูกค้า class หนึ่งส่งอีเมล อีก class ส่ง SMS ทั้งคู่ทำหน้าที่เดียวกันในเชิงแนวคิดคือ “ส่งข้อความหาลูกค้า” แต่เปิด interface ต่างกันโดยสิ้นเชิง
// ก่อน refactor — interface ไม่ตรงกัน แม้ทำงานเดียวกันpublic class EmailSender{ public void SendEmail(string recipient, string subject, string body) { // ... ส่งอีเมลจริง }}
public class SmsGateway{ public void Transmit(string phoneNumber, string message) { // ... ส่ง SMS จริง }}
// code ผู้เรียกต้องแยกเงื่อนไขตามชนิด objectpublic void Notify(object channel, Customer customer, string message){ if (channel is EmailSender email) { email.SendEmail(customer.Email, "แจ้งเตือนคำสั่งซื้อ", message); } else if (channel is SmsGateway sms) { sms.Transmit(customer.Phone, message); } // เพิ่มช่องทางใหม่ทีไร ต้องมาเพิ่ม else if ที่นี่ทุกครั้ง}ทางแก้ตามแนวทางของ Fowler คือหาตัวส่วนร่วม (common denominator) ของ interface แล้วปรับชื่อ method และ signature ให้ตรงกัน ด้วยการทำ Change Function Declaration (เดิมเรียก Rename Method) เพื่อรวมชื่อ method ให้เหมือนกัน จากนั้นทำ Extract Interface เพื่อประกาศสัญญากลางที่ทั้ง2 class implement ร่วมกัน
// หลัง refactor — รวม interface ด้วย Change Function Declaration + Extract Interfacepublic interface INotificationChannel{ void Send(string address, string message);}
public class EmailSender : INotificationChannel{ public void Send(string address, string message) { // ... ส่งอีเมลจริง โดย address คืออีเมล }}
public class SmsGateway : INotificationChannel{ public void Send(string address, string message) { // ... ส่ง SMS จริง โดย address คือเบอร์โทร }}
// code ผู้เรียกไม่ต้องรู้จักชนิดที่แท้จริงอีกต่อไปpublic void Notify(INotificationChannel channel, string address, string message){ channel.Send(address, message);}หลัง refactor ทั้ง2 class substitute กันได้จริงผ่าน INotificationChannel เดียวกัน เพิ่มช่องทางใหม่ (เช่น push notification) ก็แค่เขียน class ใหม่ที่ implement interface นี้ ไม่ต้องแตะ code ผู้เรียกเลย
classDiagram
INotificationChannel <|.. EmailSender
INotificationChannel <|.. SmsGateway
Notify --> INotificationChannel
ถ้า2 class มีแค่ “บางส่วน” ที่ซ้ำกัน ไม่ใช่ทั้งหมด ให้ใช้ Extract Superclass แทน — ดึงพฤติกรรมร่วมขึ้นเป็น class แม่ แล้วให้ class เดิมทั้งสองกลายเป็น subclass ของมัน ส่วนกรณีที่ class ทางเลือกมาจาก library ภายนอกคนละตัว (เช่น SDK ผู้ให้บริการอีเมลสองเจ้า) ซึ่งแก้ source เดิมไม่ได้ ให้ใช้ Adapter ห่อแต่ละตัวให้เปิด interface เดียวกันแทนที่จะพยายาม merge class จริง
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Adapter — ห่อ interface ที่ต่างกันให้เป็นหน้าตาเดียว เมื่อแก้ class เดิมไม่ได้
- Liskov Substitution — เป้าหมายปลายทางของการรวม interface คือให้ substitute กันได้จริง
- Duplicate Code — กลิ่นพี่น้องกัน เพียงแต่ซ้ำกันที่ระดับ abstraction แทนที่จะเป็นระดับ statement
- Inconsistency — ชื่อ method/parameter ที่ไม่สอดคล้องกันคือรากของปัญหานี้
- Oddball Solution — อีกกลิ่นในตระกูลเดียวกันที่เกิดจากการแก้ปัญหาซ้ำด้วยวิธีที่ต่างออกไปโดยไม่จำเป็น
- DRY — หลักการที่กลิ่นนี้ละเมิดเมื่อ code ผู้เรียกต้องคัดลอกตรรกะแยกตามชนิด class
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/code-smells/alternative-class-different-interfaces
- Alternative Classes with Different Interfaces – Refactoring.Guru
- Alternative Classes with Different Interfaces – SourceMaking
- Code Smells: Alternative Classes with Different Interfaces – Luzkan’s Code Smells Catalog
- Design smell – Wikipedia